├── .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 | [](https://packagist.org/packages/ion-bazan/composer-diff)
4 | [](https://packagist.org/packages/ion-bazan/composer-diff)
5 | [](https://packagist.org/packages/ion-bazan/composer-diff)
6 | [](https://packagist.org/packages/ion-bazan/composer-diff)
7 | [](https://github.com/IonBazan/composer-diff/actions)
8 | [](https://codecov.io/gh/IonBazan/composer-diff)
9 | [](https://dashboard.stryker-mutator.io/reports/github.com/IonBazan/composer-diff/master)
10 | [](https://packagist.org/packages/ion-bazan/composer-diff)
11 | [](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 | 
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 |