├── .styleci.yml ├── LICENSE ├── README.md ├── composer-diff ├── composer.json ├── docs ├── CONTRIBUTING.md ├── formatters.md └── url-generators.md ├── phpstan.neon ├── preview.png └── src ├── Command ├── BaseNotTypedCommand.php ├── BaseTypedCommand.php ├── CommandProvider.php └── DiffCommand.php ├── Composer └── Plugin.php ├── Diff ├── DiffEntries.php ├── DiffEntry.php └── VersionComparator.php ├── Formatter ├── AbstractFormatter.php ├── Formatter.php ├── FormatterContainer.php ├── GitHubFormatter.php ├── Helper │ ├── OutputHelper.php │ └── Table.php ├── JsonFormatter.php ├── MarkdownFormatter.php ├── MarkdownListFormatter.php └── MarkdownTableFormatter.php ├── PackageDiff.php └── Url ├── BitBucketGenerator.php ├── DrupalGenerator.php ├── GeneratorContainer.php ├── GitGenerator.php ├── GithubGenerator.php ├── GitlabGenerator.php └── UrlGenerator.php /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: symfony 2 | 3 | enabled: 4 | - long_array_syntax 5 | 6 | disabled: 7 | - short_array_syntax 8 | 9 | finder: 10 | name: 11 | - "*.php" 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Ion Bazan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composer Diff Plugin 2 | 3 | [![PHP 5.3+ | 7.x | 8.x](https://img.shields.io/badge/PHP-^5.3_|_^7_|_^8-blue.svg)](https://packagist.org/packages/ion-bazan/composer-diff) 4 | [![Composer v1 | v2](https://img.shields.io/badge/Composer-^1.1_|_^2-success.svg)](https://packagist.org/packages/ion-bazan/composer-diff) 5 | [![Dependencies: 0](https://img.shields.io/badge/dependencies-0-success.svg)](https://packagist.org/packages/ion-bazan/composer-diff) 6 | [![Latest version](https://img.shields.io/packagist/v/ion-bazan/composer-diff.svg)](https://packagist.org/packages/ion-bazan/composer-diff) 7 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/IonBazan/composer-diff/test.yml)](https://github.com/IonBazan/composer-diff/actions) 8 | [![Codecov](https://img.shields.io/codecov/c/gh/IonBazan/composer-diff)](https://codecov.io/gh/IonBazan/composer-diff) 9 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FIonBazan%2Fcomposer-diff%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/IonBazan/composer-diff/master) 10 | [![Downloads](https://img.shields.io/packagist/dt/ion-bazan/composer-diff.svg)](https://packagist.org/packages/ion-bazan/composer-diff) 11 | [![License](https://img.shields.io/packagist/l/ion-bazan/composer-diff.svg)](https://packagist.org/packages/ion-bazan/composer-diff) 12 | 13 | Generates packages changes report in Markdown format by comparing `composer.lock` files. Compares with last-committed changes by default. 14 | 15 | **Now available as [GitHub Action](https://github.com/marketplace/actions/composer-diff)!** 16 | 17 | **Web version available at https://lyrixx.github.io/composer-diff** 18 | 19 | ![preview](preview.png) 20 | 21 | # Installation 22 | 23 | ```shell script 24 | composer global require ion-bazan/composer-diff 25 | ``` 26 | 27 | # Usage 28 | 29 | ```shell script 30 | composer diff # Displays packages changed in current git tree compared with HEAD 31 | composer diff --help # Display detailed usage instructions 32 | ``` 33 | 34 | ## Example output 35 | 36 | | Prod Packages | Operation | Base | Target | 37 | |------------------------------------|-----------|---------|---------| 38 | | psr/event-dispatcher | New | - | 1.0.0 | 39 | | symfony/deprecation-contracts | New | - | v2.1.2 | 40 | | symfony/event-dispatcher | Upgraded | v2.8.52 | v5.1.2 | 41 | | symfony/event-dispatcher-contracts | New | - | v2.1.2 | 42 | | symfony/polyfill-php80 | New | - | v1.17.1 | 43 | | php | New | - | >=5.3 | 44 | 45 | | Dev Packages | Operation | Base | Target | 46 | |------------------------------------|------------|-------|--------| 47 | | phpunit/php-code-coverage | Downgraded | 8.0.2 | 7.0.10 | 48 | | phpunit/php-file-iterator | Downgraded | 3.0.2 | 2.0.2 | 49 | | phpunit/php-text-template | Downgraded | 2.0.1 | 1.2.1 | 50 | | phpunit/php-timer | Downgraded | 5.0.0 | 2.1.2 | 51 | | phpunit/php-token-stream | Downgraded | 4.0.2 | 3.1.1 | 52 | | phpunit/phpunit | Downgraded | 9.2.5 | 8.5.8 | 53 | | sebastian/code-unit-reverse-lookup | Downgraded | 2.0.1 | 1.0.1 | 54 | | sebastian/comparator | Downgraded | 4.0.2 | 3.0.2 | 55 | | sebastian/diff | Downgraded | 4.0.1 | 3.0.2 | 56 | | sebastian/environment | Downgraded | 5.1.1 | 4.2.3 | 57 | | sebastian/exporter | Downgraded | 4.0.1 | 3.1.2 | 58 | | sebastian/global-state | Downgraded | 4.0.0 | 3.0.0 | 59 | | sebastian/object-enumerator | Downgraded | 4.0.1 | 3.0.3 | 60 | | sebastian/object-reflector | Downgraded | 2.0.1 | 1.1.1 | 61 | | sebastian/recursion-context | Downgraded | 4.0.1 | 3.0.0 | 62 | | sebastian/resource-operations | Downgraded | 3.0.1 | 2.0.1 | 63 | | sebastian/type | Downgraded | 2.1.0 | 1.1.3 | 64 | | sebastian/version | Downgraded | 3.0.0 | 2.0.1 | 65 | | phpunit/php-invoker | Removed | 3.0.1 | - | 66 | | sebastian/code-unit | Removed | 1.0.3 | - | 67 | 68 | ## Options 69 | 70 | - `--base` (`-b`) - path, URL or git ref to original `composer.lock` file 71 | - `--target` (`-t`) - path, URL or git ref to modified `composer.lock` file 72 | - `--no-dev` - ignore dev dependencies (`require-dev`) 73 | - `--no-prod` - ignore prod dependencies (`require`) 74 | - `--direct` (`-D`) - only show direct dependencies 75 | - `--with-platform` (`-p`) - include platform dependencies (PHP, extensions, etc.) 76 | - `--with-links` (`-l`) - include compare/release URLs 77 | - `--with-licenses` (`-c`) - include license information 78 | - `--format` (`-f`) - output format (mdtable, mdlist, json, github) - default: `mdtable` 79 | - `--gitlab-domains` - custom gitlab domains for compare/release URLs - default: use composer config 80 | 81 | ## Advanced usage 82 | 83 | ```shell script 84 | composer diff master # Compare current composer.lock with the one on master branch 85 | composer diff master:composer.lock develop:composer.lock -p # Compare master and develop branches, including platform dependencies 86 | composer diff --no-dev # ignore dev dependencies 87 | composer diff -p # include platform dependencies 88 | composer diff -f json # Output as JSON instead of table 89 | ``` 90 | 91 | You can find more documentation in the [docs](docs) directory. 92 | 93 | ### Strict mode 94 | 95 | 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. 96 | 97 | Exit code of the command is built using following bit flags: 98 | 99 | * `0` - OK. 100 | * `1` - General error. 101 | * `2` - There were changes in prod packages. 102 | * `4` - There were changes is dev packages. 103 | * `8` - There were downgrades in prod packages. 104 | * `16` - There were downgrades in dev packages. 105 | 106 | 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. 107 | 108 | # Contributing 109 | 110 | Composer Diff is an open source project that welcomes pull requests and issues from anyone. 111 | Before opening pull requests, please consider reading our short [Contribution Guidelines](docs/CONTRIBUTING.md). 112 | 113 | # Similar packages 114 | 115 | While there are several existing packages offering similar functionality: 116 | 117 | - [jbzoo/composer-diff](https://packagist.org/packages/jbzoo/composer-diff) - requires PHP 7.2+, no composer plugin support 118 | - [josefglatz/composer-diff-plugin](https://packagist.org/packages/josefglatz/composer-diff-plugin) - works only right after install/update 119 | - [davidrjonas/composer-lock-diff](https://packagist.org/packages/davidrjonas/composer-lock-diff) - does not work as composer plugin 120 | 121 | This package offers: 122 | 123 | - Support for wide range of PHP versions, starting from 5.3.2 up to 8.0 and newer. 124 | - No dependencies if you run it as composer plugin. 125 | - Both standalone executable and composer plugin interface - you choose how you want to use it. 126 | - Allows generating reports in several formats. 127 | - Extra Gitlab domains support. 128 | - [GitHub Action](https://github.com/marketplace/actions/composer-diff) with example workflow 129 | - 100% test coverage. 130 | - MIT license. 131 | -------------------------------------------------------------------------------- /composer-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | = 2.3 to use this binary or use composer diff instead.'.PHP_EOL; 19 | exit(1); 20 | } 21 | 22 | if (!class_exists('Composer\Package\CompletePackage')) { 23 | echo 'Please install composer/composer >= 1.1 to use this binary or use composer diff instead.'.PHP_EOL; 24 | exit(1); 25 | } 26 | 27 | $application = new Application(); 28 | $application->add(new DiffCommand(new PackageDiff())); 29 | $application->setDefaultCommand('diff', true); 30 | $application->run(); 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ion-bazan/composer-diff", 3 | "type": "composer-plugin", 4 | "description": "Compares composer.lock changes and generates Markdown report so you can use it in PR description.", 5 | "keywords": [ 6 | "composer", 7 | "composer.lock", 8 | "diff", 9 | "packages", 10 | "markdown", 11 | "pullrequest", 12 | "github", 13 | "packagist" 14 | ], 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Ion Bazan", 19 | "email": "ion.bazan@gmail.com" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=5.3.2", 24 | "ext-json": "*", 25 | "composer-plugin-api": "^1.1 || ^2.0" 26 | }, 27 | "require-dev": { 28 | "composer/composer": "^1.1 || ^2.0", 29 | "symfony/console": "^2.3 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0", 30 | "symfony/phpunit-bridge": "^4.2 || ^5.0 || ^6.0 || ^7.0" 31 | }, 32 | "suggest": { 33 | "composer/composer": "To use the binary without composer runtime", 34 | "symfony/console": "To use the binary without composer runtime" 35 | }, 36 | "config": { 37 | "platform-check": false 38 | }, 39 | "extra": { 40 | "class": "IonBazan\\ComposerDiff\\Composer\\Plugin" 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "IonBazan\\ComposerDiff\\": "src" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "IonBazan\\ComposerDiff\\Tests\\": "tests/" 50 | } 51 | }, 52 | "bin": [ 53 | "composer-diff" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | Any contributions (issues, pull requests, code review, ideas, etc.) are welcome. 4 | Here are a few guidelines to be aware of: 5 | 6 | - Include tests for any new features or bug fixes. 7 | - All new features should target the `main` branch. 8 | - All code must follow the project coding style standards which will be enforced by [StyleCI](https://styleci.io/). 9 | 10 | Since this project has rather strict coding standards, which include: 11 | - PHPStan level 6 12 | - 100% test coverage 13 | - 100% MSI (Mutation Score Indicator) 14 | - Compatibility with PHP 5.3.2 up to 8.0 and newer 15 | 16 | It might be a bit challenging to get started but fret not, 17 | as the maintainers will be happy to assist you should you have any questions or need help with your contribution. 18 | Even if the tests fail, don't worry, as the maintainers will help you fix them. 19 | 20 | ## Getting started 21 | 22 | 1. Fork the repository on GitHub. 23 | 2. Clone your fork locally. 24 | 3. Run `composer install` to install the dependencies. 25 | 4. Create a new branch for your feature or bug fix. 26 | 5. Write code and tests for your new feature or bug fix. 27 | 6. Run the tests (`vendor/bin/simple-phpunit`) to be sure everything is working. 28 | 7. Push your branch to your fork on GitHub. 29 | 8. Create a pull request to the `main` branch. 30 | 9. Wait for the maintainers to review your pull request. 31 | 32 | ## Testing against a real project 33 | 34 | Sometimes it's easier to check your changes when you have an actual project where a bug occurs rather than reproducing it in a test. 35 | While this package works as a Composer plugin, it might seem difficult to test your changes against a real project. 36 | 37 | Consider a following example: 38 | 39 | - `~/work/my-project` - path to my project 40 | - `~/work/composer-diff` - path to this repository 41 | 42 | Running the `composer-diff` command with your changes against your project is as simple as: 43 | 44 | ```shell 45 | cd ~/work/my-project # Navigate to your project directory 46 | ~/work/composer-diff/composer-diff # Run the composer-diff command from this repository 47 | ``` 48 | 49 | You can specify any other options as well like `--no-dev`, `--with-platform`, etc. 50 | -------------------------------------------------------------------------------- /docs/formatters.md: -------------------------------------------------------------------------------- 1 | # Output formatters 2 | 3 | There are currently four output formats available: 4 | 5 | - `mdtable` - Markdown table (default) 6 | - `mdlist` - Markdown list 7 | - `json` - JSON 8 | - `github` - GitHub Annotations 9 | 10 | You can select the output format using the `--format` (`-f`) option. 11 | 12 | ```shell script 13 | composer diff --format mdlist 14 | composer diff -f json 15 | ``` 16 | 17 | ## Markdown table (mdtable) 18 | 19 | This is the default output format. It will display the changes in a table format. 20 | 21 | Example output: 22 | 23 | ``` 24 | | Prod Packages | Operation | Base | Target | 25 | |------------------------------------|-----------|--------------------|--------------------| 26 | | psr/event-dispatcher | New | - | 1.0.0 | 27 | | roave/security-advisories | Changed | dev-master 3c97c13 | dev-master ac36586 | 28 | | symfony/deprecation-contracts | New | - | v2.1.2 | 29 | | symfony/event-dispatcher | Upgraded | v2.8.52 | v5.1.2 | 30 | | symfony/event-dispatcher-contracts | New | - | v2.1.2 | 31 | | symfony/polyfill-php80 | New | - | v1.17.1 | 32 | 33 | | Dev Packages | Operation | Base | Target | 34 | |------------------------------------|------------|-------|--------| 35 | | phpunit/php-code-coverage | Downgraded | 8.0.2 | 7.0.10 | 36 | | phpunit/php-file-iterator | Downgraded | 3.0.2 | 2.0.2 | 37 | | phpunit/php-text-template | Downgraded | 2.0.1 | 1.2.1 | 38 | | phpunit/php-timer | Downgraded | 5.0.0 | 2.1.2 | 39 | | phpunit/php-token-stream | Downgraded | 4.0.2 | 3.1.1 | 40 | | phpunit/phpunit | Downgraded | 9.2.5 | 8.5.8 | 41 | | sebastian/code-unit-reverse-lookup | Downgraded | 2.0.1 | 1.0.1 | 42 | | sebastian/comparator | Downgraded | 4.0.2 | 3.0.2 | 43 | | sebastian/diff | Downgraded | 4.0.1 | 3.0.2 | 44 | | sebastian/environment | Downgraded | 5.1.1 | 4.2.3 | 45 | | sebastian/exporter | Downgraded | 4.0.1 | 3.1.2 | 46 | | sebastian/global-state | Downgraded | 4.0.0 | 3.0.0 | 47 | | sebastian/object-enumerator | Downgraded | 4.0.1 | 3.0.3 | 48 | | sebastian/object-reflector | Downgraded | 2.0.1 | 1.1.1 | 49 | | sebastian/recursion-context | Downgraded | 4.0.1 | 3.0.0 | 50 | | sebastian/resource-operations | Downgraded | 3.0.1 | 2.0.1 | 51 | | sebastian/type | Downgraded | 2.1.0 | 1.1.3 | 52 | | sebastian/version | Downgraded | 3.0.0 | 2.0.1 | 53 | | phpunit/php-invoker | Removed | 3.0.1 | - | 54 | | sebastian/code-unit | Removed | 1.0.3 | - | 55 | ``` 56 | 57 | Rendered output: 58 | 59 | | Prod Packages | Operation | Base | Target | 60 | |------------------------------------|-----------|--------------------|--------------------| 61 | | psr/event-dispatcher | New | - | 1.0.0 | 62 | | roave/security-advisories | Changed | dev-master 3c97c13 | dev-master ac36586 | 63 | | symfony/deprecation-contracts | New | - | v2.1.2 | 64 | | symfony/event-dispatcher | Upgraded | v2.8.52 | v5.1.2 | 65 | | symfony/event-dispatcher-contracts | New | - | v2.1.2 | 66 | | symfony/polyfill-php80 | New | - | v1.17.1 | 67 | 68 | | Dev Packages | Operation | Base | Target | 69 | |------------------------------------|------------|-------|--------| 70 | | phpunit/php-code-coverage | Downgraded | 8.0.2 | 7.0.10 | 71 | | phpunit/php-file-iterator | Downgraded | 3.0.2 | 2.0.2 | 72 | | phpunit/php-text-template | Downgraded | 2.0.1 | 1.2.1 | 73 | | phpunit/php-timer | Downgraded | 5.0.0 | 2.1.2 | 74 | | phpunit/php-token-stream | Downgraded | 4.0.2 | 3.1.1 | 75 | | phpunit/phpunit | Downgraded | 9.2.5 | 8.5.8 | 76 | | sebastian/code-unit-reverse-lookup | Downgraded | 2.0.1 | 1.0.1 | 77 | | sebastian/comparator | Downgraded | 4.0.2 | 3.0.2 | 78 | | sebastian/diff | Downgraded | 4.0.1 | 3.0.2 | 79 | | sebastian/environment | Downgraded | 5.1.1 | 4.2.3 | 80 | | sebastian/exporter | Downgraded | 4.0.1 | 3.1.2 | 81 | | sebastian/global-state | Downgraded | 4.0.0 | 3.0.0 | 82 | | sebastian/object-enumerator | Downgraded | 4.0.1 | 3.0.3 | 83 | | sebastian/object-reflector | Downgraded | 2.0.1 | 1.1.1 | 84 | | sebastian/recursion-context | Downgraded | 4.0.1 | 3.0.0 | 85 | | sebastian/resource-operations | Downgraded | 3.0.1 | 2.0.1 | 86 | | sebastian/type | Downgraded | 2.1.0 | 1.1.3 | 87 | | sebastian/version | Downgraded | 3.0.0 | 2.0.1 | 88 | | phpunit/php-invoker | Removed | 3.0.1 | - | 89 | | sebastian/code-unit | Removed | 1.0.3 | - | 90 | 91 | ## Markdown list (mdlist) 92 | 93 | This format will display the changes in a markdown list format. 94 | 95 | Example output: 96 | 97 | ``` 98 | Prod Packages 99 | ============= 100 | 101 | - Install psr/event-dispatcher (1.0.0) 102 | - Change roave/security-advisories (dev-master 3c97c13 => dev-master ac36586) 103 | - Install symfony/deprecation-contracts (v2.1.2) 104 | - Upgrade symfony/event-dispatcher (v2.8.52 => v5.1.2) 105 | - Install symfony/event-dispatcher-contracts (v2.1.2) 106 | - Install symfony/polyfill-php80 (v1.17.1) 107 | 108 | Dev Packages 109 | ============ 110 | 111 | - Downgrade phpunit/php-code-coverage (8.0.2 => 7.0.10) 112 | - Downgrade phpunit/php-file-iterator (3.0.2 => 2.0.2) 113 | - Downgrade phpunit/php-text-template (2.0.1 => 1.2.1) 114 | - Downgrade phpunit/php-timer (5.0.0 => 2.1.2) 115 | - Downgrade phpunit/php-token-stream (4.0.2 => 3.1.1) 116 | - Downgrade phpunit/phpunit (9.2.5 => 8.5.8) 117 | - Downgrade sebastian/code-unit-reverse-lookup (2.0.1 => 1.0.1) 118 | - Downgrade sebastian/comparator (4.0.2 => 3.0.2) 119 | - Downgrade sebastian/diff (4.0.1 => 3.0.2) 120 | - Downgrade sebastian/environment (5.1.1 => 4.2.3) 121 | - Downgrade sebastian/exporter (4.0.1 => 3.1.2) 122 | - Downgrade sebastian/global-state (4.0.0 => 3.0.0) 123 | - Downgrade sebastian/object-enumerator (4.0.1 => 3.0.3) 124 | - Downgrade sebastian/object-reflector (2.0.1 => 1.1.1) 125 | - Downgrade sebastian/recursion-context (4.0.1 => 3.0.0) 126 | - Downgrade sebastian/resource-operations (3.0.1 => 2.0.1) 127 | - Downgrade sebastian/type (2.1.0 => 1.1.3) 128 | - Downgrade sebastian/version (3.0.0 => 2.0.1) 129 | - Uninstall phpunit/php-invoker (3.0.1) 130 | - Uninstall sebastian/code-unit (1.0.3) 131 | ``` 132 | 133 | Rendered output: 134 | 135 | Prod Packages 136 | ============= 137 | 138 | - Install psr/event-dispatcher (1.0.0) 139 | - Change roave/security-advisories (dev-master 3c97c13 => dev-master ac36586) 140 | - Install symfony/deprecation-contracts (v2.1.2) 141 | - Upgrade symfony/event-dispatcher (v2.8.52 => v5.1.2) 142 | - Install symfony/event-dispatcher-contracts (v2.1.2) 143 | - Install symfony/polyfill-php80 (v1.17.1) 144 | 145 | Dev Packages 146 | ============ 147 | 148 | - Downgrade phpunit/php-code-coverage (8.0.2 => 7.0.10) 149 | - Downgrade phpunit/php-file-iterator (3.0.2 => 2.0.2) 150 | - Downgrade phpunit/php-text-template (2.0.1 => 1.2.1) 151 | - Downgrade phpunit/php-timer (5.0.0 => 2.1.2) 152 | - Downgrade phpunit/php-token-stream (4.0.2 => 3.1.1) 153 | - Downgrade phpunit/phpunit (9.2.5 => 8.5.8) 154 | - Downgrade sebastian/code-unit-reverse-lookup (2.0.1 => 1.0.1) 155 | - Downgrade sebastian/comparator (4.0.2 => 3.0.2) 156 | - Downgrade sebastian/diff (4.0.1 => 3.0.2) 157 | - Downgrade sebastian/environment (5.1.1 => 4.2.3) 158 | - Downgrade sebastian/exporter (4.0.1 => 3.1.2) 159 | - Downgrade sebastian/global-state (4.0.0 => 3.0.0) 160 | - Downgrade sebastian/object-enumerator (4.0.1 => 3.0.3) 161 | - Downgrade sebastian/object-reflector (2.0.1 => 1.1.1) 162 | - Downgrade sebastian/recursion-context (4.0.1 => 3.0.0) 163 | - Downgrade sebastian/resource-operations (3.0.1 => 2.0.1) 164 | - Downgrade sebastian/type (2.1.0 => 1.1.3) 165 | - Downgrade sebastian/version (3.0.0 => 2.0.1) 166 | - Uninstall phpunit/php-invoker (3.0.1) 167 | - Uninstall sebastian/code-unit (1.0.3) 168 | 169 | 170 | ## JSON (json) 171 | 172 | This format will display the changes in a JSON format for parsing by other tools. 173 | 174 | Example output: 175 | 176 | ```json 177 | { 178 | "packages": { 179 | "psr\/event-dispatcher": { 180 | "name": "psr\/event-dispatcher", 181 | "operation": "install", 182 | "version_base": null, 183 | "version_target": "1.0.0" 184 | }, 185 | "roave\/security-advisories": { 186 | "name": "roave\/security-advisories", 187 | "operation": "change", 188 | "version_base": "dev-master 3c97c13", 189 | "version_target": "dev-master ac36586" 190 | }, 191 | "symfony\/deprecation-contracts": { 192 | "name": "symfony\/deprecation-contracts", 193 | "operation": "install", 194 | "version_base": null, 195 | "version_target": "v2.1.2" 196 | }, 197 | "symfony\/event-dispatcher": { 198 | "name": "symfony\/event-dispatcher", 199 | "operation": "upgrade", 200 | "version_base": "v2.8.52", 201 | "version_target": "v5.1.2" 202 | }, 203 | "symfony\/event-dispatcher-contracts": { 204 | "name": "symfony\/event-dispatcher-contracts", 205 | "operation": "install", 206 | "version_base": null, 207 | "version_target": "v2.1.2" 208 | }, 209 | "symfony\/polyfill-php80": { 210 | "name": "symfony\/polyfill-php80", 211 | "operation": "install", 212 | "version_base": null, 213 | "version_target": "v1.17.1" 214 | } 215 | }, 216 | "packages-dev": { 217 | "phpunit\/php-code-coverage": { 218 | "name": "phpunit\/php-code-coverage", 219 | "operation": "downgrade", 220 | "version_base": "8.0.2", 221 | "version_target": "7.0.10" 222 | }, 223 | "phpunit\/php-file-iterator": { 224 | "name": "phpunit\/php-file-iterator", 225 | "operation": "downgrade", 226 | "version_base": "3.0.2", 227 | "version_target": "2.0.2" 228 | }, 229 | "phpunit\/php-text-template": { 230 | "name": "phpunit\/php-text-template", 231 | "operation": "downgrade", 232 | "version_base": "2.0.1", 233 | "version_target": "1.2.1" 234 | }, 235 | "phpunit\/php-timer": { 236 | "name": "phpunit\/php-timer", 237 | "operation": "downgrade", 238 | "version_base": "5.0.0", 239 | "version_target": "2.1.2" 240 | }, 241 | "phpunit\/php-token-stream": { 242 | "name": "phpunit\/php-token-stream", 243 | "operation": "downgrade", 244 | "version_base": "4.0.2", 245 | "version_target": "3.1.1" 246 | }, 247 | "phpunit\/phpunit": { 248 | "name": "phpunit\/phpunit", 249 | "operation": "downgrade", 250 | "version_base": "9.2.5", 251 | "version_target": "8.5.8" 252 | }, 253 | "sebastian\/code-unit-reverse-lookup": { 254 | "name": "sebastian\/code-unit-reverse-lookup", 255 | "operation": "downgrade", 256 | "version_base": "2.0.1", 257 | "version_target": "1.0.1" 258 | }, 259 | "sebastian\/comparator": { 260 | "name": "sebastian\/comparator", 261 | "operation": "downgrade", 262 | "version_base": "4.0.2", 263 | "version_target": "3.0.2" 264 | }, 265 | "sebastian\/diff": { 266 | "name": "sebastian\/diff", 267 | "operation": "downgrade", 268 | "version_base": "4.0.1", 269 | "version_target": "3.0.2" 270 | }, 271 | "sebastian\/environment": { 272 | "name": "sebastian\/environment", 273 | "operation": "downgrade", 274 | "version_base": "5.1.1", 275 | "version_target": "4.2.3" 276 | }, 277 | "sebastian\/exporter": { 278 | "name": "sebastian\/exporter", 279 | "operation": "downgrade", 280 | "version_base": "4.0.1", 281 | "version_target": "3.1.2" 282 | }, 283 | "sebastian\/global-state": { 284 | "name": "sebastian\/global-state", 285 | "operation": "downgrade", 286 | "version_base": "4.0.0", 287 | "version_target": "3.0.0" 288 | }, 289 | "sebastian\/object-enumerator": { 290 | "name": "sebastian\/object-enumerator", 291 | "operation": "downgrade", 292 | "version_base": "4.0.1", 293 | "version_target": "3.0.3" 294 | }, 295 | "sebastian\/object-reflector": { 296 | "name": "sebastian\/object-reflector", 297 | "operation": "downgrade", 298 | "version_base": "2.0.1", 299 | "version_target": "1.1.1" 300 | }, 301 | "sebastian\/recursion-context": { 302 | "name": "sebastian\/recursion-context", 303 | "operation": "downgrade", 304 | "version_base": "4.0.1", 305 | "version_target": "3.0.0" 306 | }, 307 | "sebastian\/resource-operations": { 308 | "name": "sebastian\/resource-operations", 309 | "operation": "downgrade", 310 | "version_base": "3.0.1", 311 | "version_target": "2.0.1" 312 | }, 313 | "sebastian\/type": { 314 | "name": "sebastian\/type", 315 | "operation": "downgrade", 316 | "version_base": "2.1.0", 317 | "version_target": "1.1.3" 318 | }, 319 | "sebastian\/version": { 320 | "name": "sebastian\/version", 321 | "operation": "downgrade", 322 | "version_base": "3.0.0", 323 | "version_target": "2.0.1" 324 | }, 325 | "phpunit\/php-invoker": { 326 | "name": "phpunit\/php-invoker", 327 | "operation": "remove", 328 | "version_base": "3.0.1", 329 | "version_target": null 330 | }, 331 | "sebastian\/code-unit": { 332 | "name": "sebastian\/code-unit", 333 | "operation": "remove", 334 | "version_base": "1.0.3", 335 | "version_target": null 336 | } 337 | } 338 | } 339 | ``` 340 | 341 | ## GitHub Annotations (github) 342 | 343 | This format will display the changes in a format that can be used as GitHub annotation notices. 344 | 345 | Example output: 346 | 347 | ``` 348 | ::notice title=Prod Packages:: - Install psr/event-dispatcher (1.0.0)%0A - Change roave/security-advisories (dev-master 3c97c13 => dev-master ac36586)%0A - Install symfony/deprecation-contracts (v2.1.2)%0A - Upgrade symfony/event-dispatcher (v2.8.52 => v5.1.2)%0A - Install symfony/event-dispatcher-contracts (v2.1.2)%0A - Install symfony/polyfill-php80 (v1.17.1) 349 | ::notice title=Dev Packages:: - Downgrade phpunit/php-code-coverage (8.0.2 => 7.0.10)%0A - Downgrade phpunit/php-file-iterator (3.0.2 => 2.0.2)%0A - Downgrade phpunit/php-text-template (2.0.1 => 1.2.1)%0A - Downgrade phpunit/php-timer (5.0.0 => 2.1.2)%0A - Downgrade phpunit/php-token-stream (4.0.2 => 3.1.1)%0A - Downgrade phpunit/phpunit (9.2.5 => 8.5.8)%0A - Downgrade sebastian/code-unit-reverse-lookup (2.0.1 => 1.0.1)%0A - Downgrade sebastian/comparator (4.0.2 => 3.0.2)%0A - Downgrade sebastian/diff (4.0.1 => 3.0.2)%0A - Downgrade sebastian/environment (5.1.1 => 4.2.3)%0A - Downgrade sebastian/exporter (4.0.1 => 3.1.2)%0A - Downgrade sebastian/global-state (4.0.0 => 3.0.0)%0A - Downgrade sebastian/object-enumerator (4.0.1 => 3.0.3)%0A - Downgrade sebastian/object-reflector (2.0.1 => 1.1.1)%0A - Downgrade sebastian/recursion-context (4.0.1 => 3.0.0)%0A - Downgrade sebastian/resource-operations (3.0.1 => 2.0.1)%0A - Downgrade sebastian/type (2.1.0 => 1.1.3)%0A - Downgrade sebastian/version (3.0.0 => 2.0.1)%0A - Uninstall phpunit/php-invoker (3.0.1)%0A - Uninstall sebastian/code-unit (1.0.3) 350 | ``` 351 | 352 | # Contributing 353 | 354 | All formatters are implemented as separate classes in the `IonBazan\ComposerDiff\Formatter` namespace 355 | and must implement the `IonBazan\ComposerDiff\Formatter\FormatterInterface` interface. 356 | 357 | If you would like to create a new formatter, create a new class in the `Formatter` namespace and register it in `FormatterContainer`. 358 | -------------------------------------------------------------------------------- /docs/url-generators.md: -------------------------------------------------------------------------------- 1 | # URL Generators 2 | 3 | URL generators are used to generate URLs for the packages listed in a diff: 4 | 5 | - `GitHubGenerator`: Generates URLs for GitHub repositories. 6 | - `GitLabGenerator`: Generates URLs for GitLab repositories. Supports custom domains. 7 | - `BitbucketGenerator`: Generates URLs for Bitbucket repositories. 8 | - `DrupalGenerator`: Generates URLs for Drupal packages. 9 | 10 | They are chosen automatically based on the package URL or other conditions specified in `supportsPackage()` method. 11 | 12 | Each generator must have following methods: 13 | 14 | - `supportsPackage()`: Checks if the generator supports the package. 15 | - `getCompareUrl()`: Generates URL for comparing two versions of the package. 16 | - `getReleaseUrl()`: Generates URL for viewing a release or commit of a package. 17 | - `getProjectUrl()`: Mainly used to generate URL to the project repository root. 18 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | level: 7 3 | paths: 4 | - src 5 | excludePaths: 6 | - src/Command/BaseNotTypedCommand.php 7 | bootstrapFiles: 8 | - src/Command/DiffCommand.php # contains class alias 9 | -------------------------------------------------------------------------------- /preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IonBazan/composer-diff/5eb28baa038045f5a161c958847e6d9920c79191/preview.png -------------------------------------------------------------------------------- /src/Command/BaseNotTypedCommand.php: -------------------------------------------------------------------------------- 1 | handle($input, $output); 19 | } 20 | 21 | /** 22 | * @return int 23 | */ 24 | abstract protected function handle(InputInterface $input, OutputInterface $output); 25 | } 26 | -------------------------------------------------------------------------------- /src/Command/BaseTypedCommand.php: -------------------------------------------------------------------------------- 1 | handle($input, $output); 19 | } 20 | 21 | /** 22 | * @return int 23 | */ 24 | abstract protected function handle(InputInterface $input, OutputInterface $output); 25 | } 26 | -------------------------------------------------------------------------------- /src/Command/CommandProvider.php: -------------------------------------------------------------------------------- 1 | composer = $args['composer']; 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function getCommands() 28 | { 29 | return array(new DiffCommand(new PackageDiff(), $this->composer->getConfig()->get('gitlab-domains'))); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Command/DiffCommand.php: -------------------------------------------------------------------------------- 1 | = 70000 21 | ? 'IonBazan\ComposerDiff\Command\BaseTypedCommand' 22 | : 'IonBazan\ComposerDiff\Command\BaseNotTypedCommand', 23 | 'IonBazan\ComposerDiff\Command\BaseCommand' 24 | ); 25 | 26 | class DiffCommand extends BaseCommand 27 | { 28 | const CHANGES_PROD = 2; 29 | const CHANGES_DEV = 4; 30 | const DOWNGRADES_PROD = 8; 31 | const DOWNGRADES_DEV = 16; 32 | /** 33 | * @var PackageDiff 34 | */ 35 | protected $packageDiff; 36 | 37 | /** 38 | * @var string[] 39 | */ 40 | protected $gitlabDomains; 41 | 42 | /** 43 | * @param string[] $gitlabDomains 44 | */ 45 | public function __construct(PackageDiff $packageDiff, array $gitlabDomains = array()) 46 | { 47 | $this->packageDiff = $packageDiff; 48 | $this->gitlabDomains = $gitlabDomains; 49 | 50 | parent::__construct(); 51 | } 52 | 53 | /** 54 | * @return void 55 | */ 56 | protected function configure() 57 | { 58 | $this->setName('diff') 59 | ->setDescription('Compares composer.lock files and shows package changes') 60 | ->addArgument('base', InputArgument::OPTIONAL, 'Base (original) composer.lock file path or git ref') 61 | ->addArgument('target', InputArgument::OPTIONAL, 'Target (modified) composer.lock file path or git ref') 62 | ->addOption('base', 'b', InputOption::VALUE_REQUIRED, 'Base (original) composer.lock file path or git ref', 'HEAD:composer.lock') 63 | ->addOption('target', 't', InputOption::VALUE_REQUIRED, 'Target (modified) composer.lock file path or git ref', 'composer.lock') 64 | ->addOption('no-dev', null, InputOption::VALUE_NONE, 'Ignore dev dependencies') 65 | ->addOption('no-prod', null, InputOption::VALUE_NONE, 'Ignore prod dependencies') 66 | ->addOption('direct', 'D', InputOption::VALUE_NONE, 'Restricts the list of packages to your direct dependencies') 67 | ->addOption('with-platform', 'p', InputOption::VALUE_NONE, 'Include platform dependencies (PHP version, extensions, etc.)') 68 | ->addOption('with-links', 'l', InputOption::VALUE_NONE, 'Include compare/release URLs') 69 | ->addOption('with-licenses', 'c', InputOption::VALUE_NONE, 'Include licenses') 70 | ->addOption('format', 'f', InputOption::VALUE_REQUIRED, 'Output format (mdtable, mdlist, json, github)', 'mdtable') 71 | ->addOption('gitlab-domains', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Extra Gitlab domains (inherited from Composer config by default)', array()) 72 | ->addOption('strict', 's', InputOption::VALUE_NONE, 'Return non-zero exit code if there are any changes') 73 | ->setHelp(<<<'EOF' 74 | The %command.name% command displays all dependency changes between two composer.lock files. 75 | 76 | By default, it will compare current filesystem changes with git HEAD: 77 | 78 | %command.full_name% 79 | 80 | To compare with specific branch, pass its name as argument: 81 | 82 | %command.full_name% master 83 | 84 | You can specify any valid git refs to compare with: 85 | 86 | %command.full_name% HEAD~3 be4aabc 87 | 88 | You can also use more verbose syntax for base and target options: 89 | 90 | %command.full_name% --base master --target composer.lock 91 | 92 | To compare files in specific path, use following syntax: 93 | 94 | %command.full_name% master:subdirectory/composer.lock /path/to/another/composer.lock 95 | 96 | By default, platform dependencies are hidden. Add --with-platform option to include them in the report: 97 | 98 | %command.full_name% --with-platform 99 | 100 | By default, transient dependencies are displayed. Add --direct option to only show direct dependencies: 101 | 102 | %command.full_name% --direct 103 | 104 | Use --with-links to include release and compare URLs in the report: 105 | 106 | %command.full_name% --with-links 107 | 108 | You can customize output format by specifying it with --format option. Choose between mdtable, mdlist and json: 109 | 110 | %command.full_name% --format=json 111 | 112 | Hide dev dependencies using --no-dev option: 113 | 114 | %command.full_name% --no-dev 115 | 116 | Passing --strict option may help you to disallow changes or downgrades by returning non-zero exit code: 117 | 118 | %command.full_name% --strict 119 | 120 | Exit code 121 | --------- 122 | 123 | Exit code of the command is built using following bit flags: 124 | 125 | * 0 - OK. 126 | * 1 - General error. 127 | * 2 - There were changes in prod packages. 128 | * 4 - There were changes is dev packages. 129 | * 8 - There were downgrades in prod packages. 130 | * 16 - There were downgrades in dev packages. 131 | EOF 132 | ) 133 | ; 134 | } 135 | 136 | /** 137 | * @return int 138 | */ 139 | protected function handle(InputInterface $input, OutputInterface $output) 140 | { 141 | $base = null !== $input->getArgument('base') ? $input->getArgument('base') : $input->getOption('base'); 142 | $target = null !== $input->getArgument('target') ? $input->getArgument('target') : $input->getOption('target'); 143 | $onlyDirect = $input->getOption('direct'); 144 | $withPlatform = $input->getOption('with-platform'); 145 | $withUrls = $input->getOption('with-links'); 146 | $withLicenses = $input->getOption('with-licenses'); 147 | $this->gitlabDomains = array_merge($this->gitlabDomains, $input->getOption('gitlab-domains')); 148 | 149 | $urlGenerators = new GeneratorContainer($this->gitlabDomains); 150 | $formatters = new FormatterContainer($output); 151 | $formatter = $formatters->getFormatter($input->getOption('format')); 152 | 153 | $this->packageDiff->setUrlGenerator($urlGenerators); 154 | 155 | $prodOperations = new DiffEntries(array()); 156 | $devOperations = new DiffEntries(array()); 157 | 158 | if (!$input->getOption('no-prod')) { 159 | $prodOperations = $this->packageDiff->getPackageDiff($base, $target, false, $withPlatform, $onlyDirect); 160 | } 161 | 162 | if (!$input->getOption('no-dev')) { 163 | $devOperations = $this->packageDiff->getPackageDiff($base, $target, true, $withPlatform, $onlyDirect); 164 | } 165 | 166 | $formatter->render($prodOperations, $devOperations, $withUrls, $withLicenses); 167 | 168 | return $input->getOption('strict') ? $this->getExitCode($prodOperations, $devOperations) : 0; 169 | } 170 | 171 | /** 172 | * @return int Exit code 173 | */ 174 | private function getExitCode(DiffEntries $prodEntries, DiffEntries $devEntries) 175 | { 176 | $exitCode = 0; 177 | 178 | if (count($prodEntries)) { 179 | $exitCode = self::CHANGES_PROD; 180 | 181 | if ($this->hasDowngrades($prodEntries)) { 182 | $exitCode |= self::DOWNGRADES_PROD; 183 | } 184 | } 185 | 186 | if (count($devEntries)) { 187 | $exitCode |= self::CHANGES_DEV; 188 | 189 | if ($this->hasDowngrades($devEntries)) { 190 | $exitCode |= self::DOWNGRADES_DEV; 191 | } 192 | } 193 | 194 | return $exitCode; 195 | } 196 | 197 | /** 198 | * @return bool 199 | */ 200 | private function hasDowngrades(DiffEntries $entries) 201 | { 202 | /** @var DiffEntry $entry */ 203 | foreach ($entries as $entry) { 204 | if ($entry->isDowngrade()) { 205 | return true; 206 | } 207 | } 208 | 209 | return false; 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Composer/Plugin.php: -------------------------------------------------------------------------------- 1 | composer = $composer; 23 | } 24 | 25 | public function getCapabilities() 26 | { 27 | return array( 28 | 'Composer\Plugin\Capability\CommandProvider' => 'IonBazan\ComposerDiff\Command\CommandProvider', 29 | ); 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | public function deactivate(Composer $composer, IOInterface $io) 36 | { 37 | } 38 | 39 | /** 40 | * @return void 41 | */ 42 | public function uninstall(Composer $composer, IOInterface $io) 43 | { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Diff/DiffEntries.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class DiffEntries extends ArrayIterator 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Diff/DiffEntry.php: -------------------------------------------------------------------------------- 1 | operation = $operation; 43 | $this->direct = $direct; 44 | $this->type = $this->determineType(); 45 | 46 | if ($urlGenerator instanceof UrlGenerator) { 47 | $this->setUrls($urlGenerator); 48 | } 49 | } 50 | 51 | /** 52 | * @return OperationInterface 53 | */ 54 | public function getOperation() 55 | { 56 | return $this->operation; 57 | } 58 | 59 | /** 60 | * @return string 61 | */ 62 | public function getType() 63 | { 64 | return $this->type; 65 | } 66 | 67 | /** 68 | * @return bool 69 | */ 70 | public function isDirect() 71 | { 72 | return $this->direct; 73 | } 74 | 75 | /** 76 | * @return bool 77 | */ 78 | public function isInstall() 79 | { 80 | return self::TYPE_INSTALL === $this->type; 81 | } 82 | 83 | /** 84 | * @return bool 85 | */ 86 | public function isUpgrade() 87 | { 88 | return self::TYPE_UPGRADE === $this->type; 89 | } 90 | 91 | /** 92 | * @return bool 93 | */ 94 | public function isDowngrade() 95 | { 96 | return self::TYPE_DOWNGRADE === $this->type; 97 | } 98 | 99 | /** 100 | * @return bool 101 | */ 102 | public function isRemove() 103 | { 104 | return self::TYPE_REMOVE === $this->type; 105 | } 106 | 107 | /** 108 | * @return bool 109 | */ 110 | public function isChange() 111 | { 112 | return self::TYPE_CHANGE === $this->type; 113 | } 114 | 115 | /** 116 | * @return string 117 | */ 118 | public function getPackageName() 119 | { 120 | return $this->getPackage()->getName(); 121 | } 122 | 123 | /** 124 | * @return PackageInterface 125 | */ 126 | public function getPackage() 127 | { 128 | $operation = $this->getOperation(); 129 | 130 | if ($operation instanceof UpdateOperation) { 131 | return $operation->getInitialPackage(); 132 | } 133 | 134 | if ($operation instanceof InstallOperation || $operation instanceof UninstallOperation) { 135 | return $operation->getPackage(); 136 | } 137 | 138 | throw new \InvalidArgumentException('Invalid operation'); 139 | } 140 | 141 | /** 142 | * @return string[] 143 | */ 144 | public function getLicenses() 145 | { 146 | $package = $this->getPackage(); 147 | 148 | if (!$package instanceof CompletePackageInterface) { 149 | return array(); 150 | } 151 | 152 | return $package->getLicense(); 153 | } 154 | 155 | /** 156 | * @return array{ 157 | * name: string, 158 | * direct: bool, 159 | * operation: string, 160 | * version_base: string|null, 161 | * version_target: string|null, 162 | * licenses: string[], 163 | * compare: string|null, 164 | * link: string|null, 165 | * } 166 | */ 167 | public function toArray() 168 | { 169 | return array( 170 | 'name' => $this->getPackageName(), 171 | 'direct' => $this->isDirect(), 172 | 'operation' => $this->getType(), 173 | 'version_base' => $this->getBaseVersion(), 174 | 'version_target' => $this->getTargetVersion(), 175 | 'licenses' => $this->getLicenses(), 176 | 'compare' => $this->getUrl(), 177 | 'link' => $this->getProjectUrl(), 178 | ); 179 | } 180 | 181 | /** 182 | * @return string|null 183 | */ 184 | public function getBaseVersion() 185 | { 186 | if ($this->operation instanceof UpdateOperation) { 187 | return $this->operation->getInitialPackage()->getFullPrettyVersion(); 188 | } 189 | 190 | if ($this->operation instanceof UninstallOperation) { 191 | return $this->operation->getPackage()->getFullPrettyVersion(); 192 | } 193 | 194 | return null; 195 | } 196 | 197 | /** 198 | * @return string|null 199 | */ 200 | public function getTargetVersion() 201 | { 202 | if ($this->operation instanceof UpdateOperation) { 203 | return $this->operation->getTargetPackage()->getFullPrettyVersion(); 204 | } 205 | 206 | if ($this->operation instanceof InstallOperation) { 207 | return $this->operation->getPackage()->getFullPrettyVersion(); 208 | } 209 | 210 | return null; 211 | } 212 | 213 | /** 214 | * @return string|null 215 | */ 216 | public function getUrl() 217 | { 218 | return $this->compareUrl; 219 | } 220 | 221 | /** 222 | * @return string|null 223 | */ 224 | public function getProjectUrl() 225 | { 226 | return $this->projectUrl; 227 | } 228 | 229 | /** 230 | * @return void 231 | */ 232 | private function setUrls(UrlGenerator $generator) 233 | { 234 | $package = $this->getPackage(); 235 | $this->projectUrl = $generator->getProjectUrl($package); 236 | 237 | $operation = $this->getOperation(); 238 | 239 | if ($operation instanceof UpdateOperation) { 240 | $this->compareUrl = $generator->getCompareUrl($operation->getInitialPackage(), $operation->getTargetPackage()); 241 | 242 | return; 243 | } 244 | 245 | $this->compareUrl = $generator->getReleaseUrl($package); 246 | } 247 | 248 | /** 249 | * @return string 250 | */ 251 | private function determineType() 252 | { 253 | if ($this->operation instanceof InstallOperation) { 254 | return self::TYPE_INSTALL; 255 | } 256 | 257 | if ($this->operation instanceof UninstallOperation) { 258 | return self::TYPE_REMOVE; 259 | } 260 | 261 | if ($this->operation instanceof UpdateOperation) { 262 | $upgrade = VersionComparator::isUpgrade($this->operation); 263 | 264 | if (null === $upgrade) { 265 | return self::TYPE_CHANGE; 266 | } 267 | 268 | return $upgrade ? self::TYPE_UPGRADE : self::TYPE_DOWNGRADE; 269 | } 270 | 271 | return self::TYPE_CHANGE; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/Diff/VersionComparator.php: -------------------------------------------------------------------------------- 1 | normalize($operation->getInitialPackage()->getVersion()); 20 | $normalizedTo = $versionParser->normalize($operation->getTargetPackage()->getVersion()); 21 | } catch (UnexpectedValueException $e) { 22 | return null; // Consider as change if versions are not parseable 23 | } 24 | 25 | /* @infection-ignore-all False-positive, handled by build matrix with Composer 1 installed */ 26 | if ( 27 | '9999999-dev' === $normalizedFrom 28 | || '9999999-dev' === $normalizedTo // BC for Composer 1.x 29 | || 0 === strpos($normalizedFrom, 'dev-') 30 | || 0 === strpos($normalizedTo, 'dev-') 31 | ) { 32 | return null; 33 | } 34 | 35 | $sorted = Semver::sort(array($normalizedTo, $normalizedFrom)); 36 | 37 | return $sorted[0] === $normalizedFrom; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Formatter/AbstractFormatter.php: -------------------------------------------------------------------------------- 1 | output = $output; 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | protected function getDecoratedPackageName(DiffEntry $entry) 24 | { 25 | return $this->terminalLink($entry->getProjectUrl(), $entry->getPackageName()); 26 | } 27 | 28 | /** 29 | * @param string|null $url 30 | * @param string $title 31 | * 32 | * @return string 33 | */ 34 | private function terminalLink($url, $title) 35 | { 36 | if (null === $url) { 37 | return $title; 38 | } 39 | 40 | // @phpstan-ignore function.alreadyNarrowedType 41 | return method_exists('Symfony\Component\Console\Formatter\OutputFormatterStyle', 'setHref') ? sprintf('%s', $url, $title) : $title; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Formatter/Formatter.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | private $formatters; 15 | 16 | public function __construct(OutputInterface $output) 17 | { 18 | $this->formatters = array( 19 | 'mdtable' => new MarkdownTableFormatter($output), 20 | 'mdlist' => new MarkdownListFormatter($output), 21 | 'github' => new GitHubFormatter($output), 22 | 'json' => new JsonFormatter($output), 23 | ); 24 | } 25 | 26 | /** 27 | * @param string $name 28 | * 29 | * @return Formatter 30 | */ 31 | public function getFormatter($name) 32 | { 33 | if (!isset($this->formatters[$name])) { 34 | return $this->formatters[self::DEFAULT_FORMATTER]; 35 | } 36 | 37 | return $this->formatters[$name]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Formatter/GitHubFormatter.php: -------------------------------------------------------------------------------- 1 | renderSingle($prodEntries, 'Prod Packages', $withUrls, $withLicenses); 16 | $this->renderSingle($devEntries, 'Dev Packages', $withUrls, $withLicenses); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function renderSingle(DiffEntries $entries, $title, $withUrls, $withLicenses) 23 | { 24 | if (!\count($entries)) { 25 | return; 26 | } 27 | 28 | $message = str_replace("\n", '%0A', implode("\n", $this->transformEntries($entries, $withUrls, $withLicenses))); 29 | $this->output->writeln(sprintf('::notice title=%s::%s', $title, $message)); 30 | } 31 | 32 | /** 33 | * @param bool $withUrls 34 | * @param bool $withLicenses 35 | * 36 | * @return string[] 37 | */ 38 | private function transformEntries(DiffEntries $entries, $withUrls, $withLicenses) 39 | { 40 | $rows = array(); 41 | 42 | foreach ($entries as $entry) { 43 | $rows[] = $this->transformEntry($entry, $withUrls, $withLicenses); 44 | } 45 | 46 | return $rows; 47 | } 48 | 49 | /** 50 | * @param bool $withUrls 51 | * @param bool $withLicenses 52 | * 53 | * @return string 54 | */ 55 | private function transformEntry(DiffEntry $entry, $withUrls, $withLicenses) 56 | { 57 | $url = $withUrls ? $entry->getUrl() : null; 58 | $url = (null !== $url) ? ' '.$url : ''; 59 | $licenses = $withLicenses ? implode(', ', $entry->getLicenses()) : ''; 60 | $licenses = ('' !== $licenses) ? ' (License: '.$licenses.')' : ''; 61 | 62 | if ($entry->isInstall()) { 63 | return sprintf( 64 | ' - Install %s (%s)%s%s', 65 | $entry->getPackageName(), 66 | $entry->getTargetVersion(), 67 | $url, 68 | $licenses 69 | ); 70 | } 71 | 72 | if ($entry->isRemove()) { 73 | return sprintf( 74 | ' - Uninstall %s (%s)%s%s', 75 | $entry->getPackageName(), 76 | $entry->getBaseVersion(), 77 | $url, 78 | $licenses 79 | ); 80 | } 81 | 82 | return sprintf( 83 | ' - %s %s (%s => %s)%s%s', 84 | ucfirst($entry->getType()), 85 | $entry->getPackageName(), 86 | $entry->getBaseVersion(), 87 | $entry->getTargetVersion(), 88 | $url, 89 | $licenses 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Formatter/Helper/OutputHelper.php: -------------------------------------------------------------------------------- 1 | isDecorated(); 27 | $formatter->setDecorated(false); 28 | $string = preg_replace("/\033\[[^m]*m/", '', $formatter->format($string)); 29 | $formatter->setDecorated($isDecorated); 30 | 31 | return $string; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Formatter/Helper/Table.php: -------------------------------------------------------------------------------- 1 | output = $output; 34 | } 35 | 36 | /** 37 | * @param string[] $headers 38 | * 39 | * @return $this 40 | */ 41 | public function setHeaders(array $headers) 42 | { 43 | $this->headers = $headers; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * @param string[][] $rows 50 | * 51 | * @return $this 52 | */ 53 | public function setRows(array $rows) 54 | { 55 | $this->rows = array(); 56 | 57 | foreach ($rows as $row) { 58 | $this->rows[] = $row; 59 | } 60 | 61 | return $this; 62 | } 63 | 64 | /** 65 | * @return void 66 | */ 67 | public function render() 68 | { 69 | $this->renderRow($this->headers); 70 | $this->renderHorizontalLine(); 71 | 72 | foreach ($this->rows as $row) { 73 | $this->renderRow($row); 74 | } 75 | } 76 | 77 | /** 78 | * @param string[] $row 79 | * 80 | * @return void 81 | */ 82 | private function renderRow(array $row) 83 | { 84 | $this->output->writeln(sprintf('| %s |', implode(' | ', $this->prepareRow($row)))); 85 | } 86 | 87 | /** 88 | * @param string[] $row 89 | * 90 | * @return string[] 91 | */ 92 | private function prepareRow(array $row) 93 | { 94 | $line = array(); 95 | 96 | foreach ($row as $column => $cell) { 97 | $line[] = $this->prepareCell($row, $column); 98 | } 99 | 100 | return $line; 101 | } 102 | 103 | /** 104 | * @return void 105 | */ 106 | private function renderHorizontalLine() 107 | { 108 | $line = array(); 109 | 110 | foreach ($this->headers as $column => $cell) { 111 | $line[] = str_repeat('-', $this->getColumnWidth($column) + 2); 112 | } 113 | 114 | $this->output->writeln(sprintf('|%s|', implode('|', $line))); 115 | } 116 | 117 | /** 118 | * @param string[] $row 119 | * @param int $column 120 | * 121 | * @return string 122 | */ 123 | private function prepareCell(array $row, $column) 124 | { 125 | $cleanLength = OutputHelper::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]); 126 | 127 | return sprintf('%s%s', $row[$column], str_repeat(' ', $this->getColumnWidth($column) - $cleanLength)); 128 | } 129 | 130 | /** 131 | * @param int $column 132 | * 133 | * @return int 134 | */ 135 | private function getColumnWidth($column) 136 | { 137 | if (isset($this->columnWidths[$column])) { 138 | return $this->columnWidths[$column]; 139 | } 140 | 141 | $lengths = array(); 142 | 143 | foreach (array_merge(array($this->headers), $this->rows) as $row) { 144 | $lengths[] = OutputHelper::strlenWithoutDecoration($this->output->getFormatter(), $row[$column]); 145 | } 146 | 147 | return $this->columnWidths[$column] = max($lengths); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Formatter/JsonFormatter.php: -------------------------------------------------------------------------------- 1 | format(array( 16 | 'packages' => $this->transformEntries($prodEntries, $withUrls, $withLicenses), 17 | 'packages-dev' => $this->transformEntries($devEntries, $withUrls, $withLicenses), 18 | )); 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function renderSingle(DiffEntries $entries, $title, $withUrls, $withLicenses) 25 | { 26 | $this->format($this->transformEntries($entries, $withUrls, $withLicenses)); 27 | } 28 | 29 | /** 30 | * @param array>|array>> $data 31 | * 32 | * @return void 33 | */ 34 | private function format(array $data) 35 | { 36 | // @phpstan-ignore argument.type 37 | $this->output->writeln(json_encode($data, 128)); // JSON_PRETTY_PRINT 38 | } 39 | 40 | /** 41 | * @param bool $withUrls 42 | * @param bool $withLicenses 43 | * 44 | * @return array> 45 | */ 46 | private function transformEntries(DiffEntries $entries, $withUrls, $withLicenses) 47 | { 48 | $rows = array(); 49 | 50 | /** @var DiffEntry $entry */ 51 | foreach ($entries as $entry) { 52 | $row = $entry->toArray(); 53 | 54 | if (!$withUrls) { 55 | unset($row['compare'], $row['link']); 56 | } 57 | 58 | if (!$withLicenses) { 59 | unset($row['licenses']); 60 | } 61 | 62 | $rows[$row['name']] = $row; 63 | } 64 | 65 | return $rows; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Formatter/MarkdownFormatter.php: -------------------------------------------------------------------------------- 1 | renderSingle($prodEntries, 'Prod Packages', $withUrls, $withLicenses); 16 | $this->renderSingle($devEntries, 'Dev Packages', $withUrls, $withLicenses); 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function renderSingle(DiffEntries $entries, $title, $withUrls, $withLicenses) 23 | { 24 | if (!\count($entries)) { 25 | return; 26 | } 27 | 28 | $this->output->writeln($title); 29 | $this->output->writeln(str_repeat('=', strlen($title))); 30 | $this->output->writeln(''); 31 | 32 | foreach ($entries as $entry) { 33 | $this->output->writeln($this->getRow($entry, $withUrls, $withLicenses)); 34 | } 35 | 36 | $this->output->writeln(''); 37 | } 38 | 39 | /** 40 | * @param bool $withUrls 41 | * @param bool $withLicenses 42 | * 43 | * @return string 44 | */ 45 | private function getRow(DiffEntry $entry, $withUrls, $withLicenses) 46 | { 47 | $url = $withUrls ? $this->formatUrl($entry->getUrl(), 'Compare') : null; 48 | $url = (null !== $url && '' !== $url) ? ' '.$url : ''; 49 | $licenses = $withLicenses ? implode(', ', $entry->getLicenses()) : ''; 50 | $licenses = ('' !== $licenses) ? ' (License: '.$licenses.')' : ''; 51 | 52 | $packageName = $entry->getPackageName(); 53 | $packageUrl = $withUrls ? $this->formatUrl($entry->getProjectUrl(), $packageName) : $packageName; 54 | 55 | if ($entry->isInstall()) { 56 | return sprintf( 57 | ' - Install %s (%s)%s%s', 58 | $packageUrl ?: $packageName, 59 | $entry->getTargetVersion(), 60 | $url, 61 | $licenses 62 | ); 63 | } 64 | 65 | if ($entry->isRemove()) { 66 | return sprintf( 67 | ' - Uninstall %s (%s)%s%s', 68 | $packageUrl ?: $packageName, 69 | $entry->getBaseVersion(), 70 | $url, 71 | $licenses 72 | ); 73 | } 74 | 75 | return sprintf( 76 | ' - %s %s (%s => %s)%s%s', 77 | ucfirst($entry->getType()), 78 | $packageUrl ?: $packageName, 79 | $entry->getBaseVersion(), 80 | $entry->getTargetVersion(), 81 | $url, 82 | $licenses 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Formatter/MarkdownTableFormatter.php: -------------------------------------------------------------------------------- 1 | renderSingle($prodEntries, 'Prod Packages', $withUrls, $withLicenses); 17 | $this->renderSingle($devEntries, 'Dev Packages', $withUrls, $withLicenses); 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function renderSingle(DiffEntries $entries, $title, $withUrls, $withLicenses) 24 | { 25 | if (!\count($entries)) { 26 | return; 27 | } 28 | 29 | $rows = array(); 30 | 31 | foreach ($entries as $entry) { 32 | $row = $this->getTableRow($entry, $withUrls); 33 | 34 | if ($withUrls) { 35 | $row[] = $this->formatUrl($entry->getUrl(), 'Compare'); 36 | } 37 | 38 | if ($withLicenses) { 39 | $row[] = implode(', ', $entry->getLicenses()); 40 | } 41 | 42 | $rows[] = $row; 43 | } 44 | 45 | $table = new Table($this->output); 46 | $headers = array($title, 'Operation', 'Base', 'Target'); 47 | 48 | if ($withUrls) { 49 | $headers[] = 'Link'; 50 | } 51 | 52 | if ($withLicenses) { 53 | $headers[] = 'License'; 54 | } 55 | 56 | $table->setHeaders($headers)->setRows($rows)->render(); 57 | $this->output->writeln(''); 58 | } 59 | 60 | /** 61 | * @param bool $withUrls 62 | * 63 | * @return string[] 64 | */ 65 | private function getTableRow(DiffEntry $entry, $withUrls) 66 | { 67 | $packageName = $this->getDecoratedPackageName($entry); 68 | $packageUrl = $withUrls ? $this->formatUrl($entry->getProjectUrl(), $packageName) : $packageName; 69 | 70 | if ($entry->isInstall()) { 71 | return array( 72 | $packageUrl ?: $packageName, 73 | 'New', 74 | '-', 75 | $entry->getTargetVersion(), 76 | ); 77 | } 78 | 79 | if ($entry->isRemove()) { 80 | return array( 81 | $packageUrl ?: $packageName, 82 | 'Removed', 83 | $entry->getBaseVersion(), 84 | '-', 85 | ); 86 | } 87 | 88 | return array( 89 | $packageUrl ?: $packageName, 90 | $entry->isChange() ? 'Changed' : ($entry->isUpgrade() ? 'Upgraded' : 'Downgraded'), 91 | $entry->getBaseVersion(), 92 | $entry->getTargetVersion(), 93 | ); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/PackageDiff.php: -------------------------------------------------------------------------------- 1 | urlGenerator = new GeneratorContainer(); 31 | } 32 | 33 | /** 34 | * @return void 35 | */ 36 | public function setUrlGenerator(UrlGenerator $urlGenerator) 37 | { 38 | $this->urlGenerator = $urlGenerator; 39 | } 40 | 41 | /** 42 | * @param string[] $directPackages 43 | * @param bool $onlyDirect 44 | * 45 | * @return DiffEntries 46 | */ 47 | public function getDiff(RepositoryInterface $oldPackages, RepositoryInterface $targetPackages, array $directPackages = array(), $onlyDirect = false) 48 | { 49 | $entries = array(); 50 | 51 | foreach ($this->getOperations($oldPackages, $targetPackages) as $operation) { 52 | $package = $operation instanceof UpdateOperation ? $operation->getTargetPackage() : $operation->getPackage(); 53 | $direct = in_array($package->getName(), $directPackages, true); 54 | 55 | if ($onlyDirect && !$direct) { 56 | continue; 57 | } 58 | 59 | $entries[] = new DiffEntry($operation, $this->urlGenerator, $direct); 60 | } 61 | 62 | return new DiffEntries($entries); 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function getOperations(RepositoryInterface $oldPackages, RepositoryInterface $targetPackages) 69 | { 70 | $operations = array(); 71 | 72 | foreach ($targetPackages->getPackages() as $newPackage) { 73 | $matchingPackages = $oldPackages->findPackages($newPackage->getName()); 74 | 75 | if ($newPackage instanceof AliasPackage) { 76 | continue; 77 | } 78 | 79 | if (0 === count($matchingPackages)) { 80 | $operations[] = new InstallOperation($newPackage); 81 | 82 | continue; 83 | } 84 | 85 | foreach ($matchingPackages as $oldPackage) { 86 | if ($oldPackage instanceof AliasPackage) { 87 | continue; 88 | } 89 | 90 | if ($oldPackage->getFullPrettyVersion() !== $newPackage->getFullPrettyVersion()) { 91 | $operations[] = new UpdateOperation($oldPackage, $newPackage); 92 | } 93 | } 94 | } 95 | 96 | foreach ($oldPackages->getPackages() as $oldPackage) { 97 | if ($oldPackage instanceof AliasPackage) { 98 | continue; 99 | } 100 | 101 | if (!$targetPackages->findPackage($oldPackage->getName(), '*')) { 102 | $operations[] = new UninstallOperation($oldPackage); 103 | } 104 | } 105 | 106 | return $operations; 107 | } 108 | 109 | /** 110 | * @param string $from 111 | * @param string $to 112 | * @param bool $dev 113 | * @param bool $withPlatform 114 | * @param bool $onlyDirect 115 | * 116 | * @return DiffEntries 117 | */ 118 | public function getPackageDiff($from, $to, $dev, $withPlatform, $onlyDirect = false) 119 | { 120 | return $this->getDiff( 121 | $this->loadPackages($from, $dev, $withPlatform), 122 | $this->loadPackages($to, $dev, $withPlatform), 123 | array_merge($this->getDirectPackages($from), $this->getDirectPackages($to)), 124 | $onlyDirect 125 | ); 126 | } 127 | 128 | /** 129 | * @param mixed[] $composerLock 130 | * @param bool $dev 131 | * @param bool $withPlatform 132 | * 133 | * @return ArrayRepository 134 | */ 135 | public function loadPackagesFromArray(array $composerLock, $dev, $withPlatform) 136 | { 137 | $loader = new ArrayLoader(); 138 | $packages = array(); 139 | $packagesKey = 'packages'.($dev ? '-dev' : ''); 140 | $platformKey = 'platform'.($dev ? '-dev' : ''); 141 | 142 | if (isset($composerLock[$packagesKey])) { 143 | foreach ($composerLock[$packagesKey] as $packageInfo) { 144 | $packages[] = $loader->load($packageInfo); 145 | } 146 | } 147 | 148 | if ($withPlatform && isset($composerLock[$platformKey])) { 149 | foreach ($composerLock[$platformKey] as $name => $version) { 150 | $packages[] = new CompletePackage($name, $version, $version); 151 | } 152 | } 153 | 154 | return new ArrayRepository($packages); 155 | } 156 | 157 | /** 158 | * @param string $path 159 | * @param bool $dev 160 | * @param bool $withPlatform 161 | * 162 | * @return ArrayRepository 163 | */ 164 | private function loadPackages($path, $dev, $withPlatform) 165 | { 166 | $data = \json_decode($this->getFileContents($path), true); 167 | 168 | return $this->loadPackagesFromArray($data, $dev, $withPlatform); 169 | } 170 | 171 | /** 172 | * @param string $path 173 | * 174 | * @return string[] 175 | */ 176 | private function getDirectPackages($path) 177 | { 178 | $data = \json_decode($this->getFileContents($path, false), true); 179 | 180 | $packages = array(); 181 | 182 | foreach (array('require', 'require-dev') as $key) { 183 | if (isset($data[$key])) { 184 | $packages = array_merge($packages, array_keys($data[$key])); 185 | } 186 | } 187 | 188 | return $packages; // @phpstan-ignore return.type 189 | } 190 | 191 | /** 192 | * @param string $path 193 | * @param bool $lockFile 194 | * 195 | * @return string 196 | */ 197 | private function getFileContents($path, $lockFile = true) 198 | { 199 | $originalPath = $path; 200 | 201 | if (empty($path)) { 202 | $path = self::COMPOSER.($lockFile ? self::EXTENSION_LOCK : self::EXTENSION_JSON); 203 | } 204 | 205 | $localPath = $path; 206 | 207 | if (!$lockFile) { 208 | $localPath = $this->getJsonPath($localPath); 209 | } 210 | 211 | if (filter_var($localPath, FILTER_VALIDATE_URL, FILTER_FLAG_PATH_REQUIRED) || file_exists($localPath)) { 212 | // @phpstan-ignore return.type 213 | return file_get_contents($localPath); 214 | } 215 | 216 | if (false === strpos($originalPath, self::GIT_SEPARATOR)) { 217 | $path .= self::GIT_SEPARATOR.self::COMPOSER.($lockFile ? self::EXTENSION_LOCK : self::EXTENSION_JSON); 218 | } 219 | 220 | if (!$lockFile) { 221 | $path = $this->getJsonPath($path); 222 | } 223 | 224 | $output = array(); 225 | @exec(sprintf('git show %s 2>&1', escapeshellarg($path)), $output, $exit); 226 | $outputString = implode("\n", $output); 227 | 228 | if (0 !== $exit) { 229 | if ($lockFile) { 230 | throw new \RuntimeException(sprintf('Could not open file %s or find it in git as %s: %s', $originalPath, $path, $outputString)); 231 | } 232 | 233 | return '{}'; // Do not throw exception for composer.json as it might not exist and that's fine 234 | } 235 | 236 | return $outputString; 237 | } 238 | 239 | /** 240 | * @param string $path 241 | * 242 | * @return string 243 | */ 244 | private function getJsonPath($path) 245 | { 246 | if (self::EXTENSION_LOCK === substr($path, -strlen(self::EXTENSION_LOCK))) { 247 | return substr($path, 0, -strlen(self::EXTENSION_LOCK)).self::EXTENSION_JSON; 248 | } 249 | 250 | return $path; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/Url/BitBucketGenerator.php: -------------------------------------------------------------------------------- 1 | supportsPackage($initialPackage) || !$this->supportsPackage($targetPackage)) { 23 | return null; 24 | } 25 | 26 | $baseUrl = $this->getRepositoryUrl($targetPackage); 27 | $baseUser = $this->getUser($initialPackage); 28 | $targetUser = $this->getUser($targetPackage); 29 | 30 | if ($baseUser === $targetUser) { 31 | return sprintf( 32 | '%s/branches/compare/%s%%0D%s', 33 | $baseUrl, 34 | $this->getCompareRef($initialPackage), 35 | $this->getCompareRef($targetPackage) 36 | ); 37 | } 38 | 39 | return sprintf( 40 | '%s/branches/compare/%s/%s:%s%%0D%s/%s:%s', 41 | $baseUrl, 42 | $baseUser, 43 | $this->getRepo($initialPackage), 44 | $this->getCompareRef($initialPackage), 45 | $targetUser, 46 | $this->getRepo($targetPackage), 47 | $this->getCompareRef($targetPackage) 48 | ); 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getReleaseUrl(PackageInterface $package) 55 | { 56 | return sprintf('%s/src/%s', $this->getRepositoryUrl($package), $package->isDev() ? $package->getSourceReference() : $package->getPrettyVersion()); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function getProjectUrl(PackageInterface $package) 63 | { 64 | return $this->getRepositoryUrl($package); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Url/DrupalGenerator.php: -------------------------------------------------------------------------------- 1 | getName() || parent::supportsPackage($package); 17 | } 18 | 19 | /** 20 | * @return string 21 | */ 22 | protected function getCompareRef(PackageInterface $package) 23 | { 24 | if (!$package->isDev()) { 25 | return $package->getDistReference(); 26 | } 27 | 28 | return parent::getCompareRef($package); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function getReleaseUrl(PackageInterface $package) 35 | { 36 | // Not sure we can support dev releases right now. Can we distinguish major version dev releases from regular branches? 37 | if ($package->isDev()) { 38 | return null; 39 | } 40 | 41 | return sprintf('%s/releases/%s', $this->getProjectUrl($package), $this->getVersionReference($package)); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function getProjectUrl(PackageInterface $package) 48 | { 49 | return sprintf('https://www.drupal.org/project/%s', $this->getDrupalProjectName($package)); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | protected function getDomain() 56 | { 57 | return 'git.drupalcode.org'; 58 | } 59 | 60 | /** 61 | * @return string|null 62 | */ 63 | private function getVersionReference(PackageInterface $package) 64 | { 65 | if ($package->getDistReference()) { 66 | return $package->getDistReference(); 67 | } 68 | 69 | return $package->getSourceReference(); 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | private function getDrupalProjectName(PackageInterface $package) 76 | { 77 | if (self::DRUPAL_CORE === $package->getName()) { 78 | return 'drupal'; 79 | } 80 | 81 | return preg_replace('/^drupal\//', '', $package->getName()); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Url/GeneratorContainer.php: -------------------------------------------------------------------------------- 1 | generators = $generators; 32 | } 33 | 34 | /** 35 | * @return UrlGenerator|null 36 | */ 37 | public function get(PackageInterface $package) 38 | { 39 | foreach ($this->generators as $generator) { 40 | if ($generator->supportsPackage($package)) { 41 | return $generator; 42 | } 43 | } 44 | 45 | return null; 46 | } 47 | 48 | public function supportsPackage(PackageInterface $package) 49 | { 50 | return null !== $this->get($package); 51 | } 52 | 53 | public function getCompareUrl(PackageInterface $initialPackage, PackageInterface $targetPackage) 54 | { 55 | if (!$generator = $this->get($targetPackage)) { 56 | return null; 57 | } 58 | 59 | return $generator->getCompareUrl($initialPackage, $targetPackage); 60 | } 61 | 62 | public function getReleaseUrl(PackageInterface $package) 63 | { 64 | if (!$generator = $this->get($package)) { 65 | return null; 66 | } 67 | 68 | return $generator->getReleaseUrl($package); 69 | } 70 | 71 | public function getProjectUrl(PackageInterface $package) 72 | { 73 | if ($generator = $this->get($package)) { 74 | return $generator->getProjectUrl($package); 75 | } 76 | 77 | if ($package instanceof CompletePackage) { 78 | return $package->getHomepage(); 79 | } 80 | 81 | return null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Url/GitGenerator.php: -------------------------------------------------------------------------------- 1 | getSourceUrl(), $this->getDomain()); 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | protected function getCompareRef(PackageInterface $package) 24 | { 25 | if (!$package->isDev()) { 26 | return $package->getPrettyVersion(); 27 | } 28 | 29 | $reference = $package->getSourceReference(); 30 | 31 | if (self::REFERENCE_LENGTH === \strlen($reference)) { 32 | return \substr($reference, 0, self::SHORT_REFERENCE_LENGTH); 33 | } 34 | 35 | return $reference; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | protected function getUser(PackageInterface $package) 42 | { 43 | return preg_replace( 44 | "/^https:\/\/{$this->getQuotedDomain()}\/(.+)\/([^\/]+)$/", 45 | '$1', 46 | $this->getRepositoryUrl($package) 47 | ); 48 | } 49 | 50 | /** 51 | * @return string 52 | */ 53 | protected function getRepo(PackageInterface $package) 54 | { 55 | return preg_replace( 56 | "/^https:\/\/{$this->getQuotedDomain()}\/(.+)\/([^\/]+)$/", 57 | '$2', 58 | $this->getRepositoryUrl($package) 59 | ); 60 | } 61 | 62 | /** 63 | * @return string 64 | */ 65 | protected function getRepositoryUrl(PackageInterface $package) 66 | { 67 | $httpsUrl = preg_replace( 68 | "/^git@(?:git\.)?({$this->getQuotedDomain()}):(.+)\/([^\/]+)(\.git)?$/", 69 | 'https://$1/$2/$3', 70 | $package->getSourceUrl() 71 | ); 72 | 73 | return preg_replace('#^(.+)\.git$#', '$1', $httpsUrl); 74 | } 75 | 76 | /** 77 | * @return string 78 | */ 79 | private function getQuotedDomain() 80 | { 81 | return preg_quote($this->getDomain(), '/'); 82 | } 83 | 84 | /** 85 | * @return string 86 | */ 87 | abstract protected function getDomain(); 88 | } 89 | -------------------------------------------------------------------------------- /src/Url/GithubGenerator.php: -------------------------------------------------------------------------------- 1 | supportsPackage($initialPackage) || !$this->supportsPackage($targetPackage)) { 15 | return null; 16 | } 17 | 18 | $baseUrl = $this->getRepositoryUrl($initialPackage); 19 | $baseMaintainer = $this->getUser($initialPackage); 20 | $targetMaintainer = $this->getUser($targetPackage); 21 | $targetVersion = ($baseMaintainer !== $targetMaintainer ? $targetMaintainer.':' : '').$this->getCompareRef($targetPackage); 22 | 23 | return sprintf('%s/compare/%s...%s', $baseUrl, $this->getCompareRef($initialPackage), $targetVersion); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getReleaseUrl(PackageInterface $package) 30 | { 31 | if ($package->isDev()) { 32 | return null; 33 | } 34 | 35 | return sprintf('%s/releases/tag/%s', $this->getRepositoryUrl($package), $package->getPrettyVersion()); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function getProjectUrl(PackageInterface $package) 42 | { 43 | return $this->getRepositoryUrl($package); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | protected function getDomain() 50 | { 51 | return 'github.com'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Url/GitlabGenerator.php: -------------------------------------------------------------------------------- 1 | domain = $domain; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function getDomain() 26 | { 27 | return $this->domain; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getCompareUrl(PackageInterface $initialPackage, PackageInterface $targetPackage) 34 | { 35 | if (!$this->supportsPackage($initialPackage) || !$this->supportsPackage($targetPackage)) { 36 | return null; 37 | } 38 | 39 | $baseUrl = $this->getRepositoryUrl($initialPackage); 40 | $baseMaintainer = $this->getUser($initialPackage); 41 | $targetMaintainer = $this->getUser($targetPackage); 42 | 43 | if ($baseMaintainer !== $targetMaintainer) { 44 | return $this->getReleaseUrl($targetPackage); // Could not get a compare URL, using release URL instead 45 | } 46 | 47 | return sprintf('%s/compare/%s...%s', $baseUrl, $this->getCompareRef($initialPackage), $this->getCompareRef($targetPackage)); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getReleaseUrl(PackageInterface $package) 54 | { 55 | if ($package->isDev()) { 56 | return null; 57 | } 58 | 59 | return sprintf('%s/tags/%s', $this->getRepositoryUrl($package), $package->getPrettyVersion()); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getProjectUrl(PackageInterface $package) 66 | { 67 | return $this->getRepositoryUrl($package); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Url/UrlGenerator.php: -------------------------------------------------------------------------------- 1 |