AssertBackwardsCompatible   A
last analyzed

Complexity

Total Complexity 11

Size/Duplication

Total Lines 200
Duplicated Lines 0 %

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 87
c 6
b 0
f 0
dl 0
loc 200
rs 10
wmc 11

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
A printOutcomeAndExit() 0 11 3
A parseRevisionFromInput() 0 7 1
A determineFromRevisionFromRepository() 0 17 1
A execute() 0 54 4
A configure() 0 26 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\BackwardCompatibility\Command;
6
7
use Assert\Assert;
8
use Roave\BackwardCompatibility\Changes;
9
use Roave\BackwardCompatibility\CompareApi;
10
use Roave\BackwardCompatibility\Factory\ComposerInstallationReflectorFactory;
11
use Roave\BackwardCompatibility\Formatter\MarkdownPipedToSymfonyConsoleFormatter;
12
use Roave\BackwardCompatibility\Formatter\SymfonyConsoleTextFormatter;
13
use Roave\BackwardCompatibility\Git\CheckedOutRepository;
14
use Roave\BackwardCompatibility\Git\GetVersionCollection;
15
use Roave\BackwardCompatibility\Git\ParseRevision;
16
use Roave\BackwardCompatibility\Git\PerformCheckoutOfRevision;
17
use Roave\BackwardCompatibility\Git\PickVersionFromVersionCollection;
18
use Roave\BackwardCompatibility\Git\Revision;
19
use Roave\BackwardCompatibility\LocateDependencies\LocateDependencies;
20
use Roave\BackwardCompatibility\Support\ArrayHelpers;
21
use Roave\BetterReflection\SourceLocator\Type\AggregateSourceLocator;
22
use Symfony\Component\Console\Command\Command;
23
use Symfony\Component\Console\Exception\InvalidArgumentException;
24
use Symfony\Component\Console\Exception\LogicException;
25
use Symfony\Component\Console\Input\InputInterface;
26
use Symfony\Component\Console\Input\InputOption;
27
use Symfony\Component\Console\Output\ConsoleOutputInterface;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use function assert;
30
use function count;
31
use function is_array;
32
use function is_string;
33
use function Safe\getcwd;
34
use function Safe\sprintf;
35
36
final class AssertBackwardsCompatible extends Command
37
{
38
    /** @var PerformCheckoutOfRevision */
39
    private $git;
40
41
    /** @var ComposerInstallationReflectorFactory */
42
    private $makeComposerInstallationReflector;
43
44
    /** @var ParseRevision */
45
    private $parseRevision;
46
47
    /** @var GetVersionCollection */
48
    private $getVersions;
49
50
    /** @var PickVersionFromVersionCollection */
51
    private $pickFromVersion;
52
53
    /** @var LocateDependencies */
54
    private $locateDependencies;
55
56
    /** @var CompareApi */
57
    private $compareApi;
58
59
    /**
60
     * @throws LogicException
61
     */
62
    public function __construct(
63
        PerformCheckoutOfRevision $git,
64
        ComposerInstallationReflectorFactory $makeComposerInstallationReflector,
65
        ParseRevision $parseRevision,
66
        GetVersionCollection $getVersions,
67
        PickVersionFromVersionCollection $pickFromVersion,
68
        LocateDependencies $locateDependencies,
69
        CompareApi $compareApi
70
    ) {
71
        parent::__construct();
72
73
        $this->git                               = $git;
74
        $this->makeComposerInstallationReflector = $makeComposerInstallationReflector;
75
        $this->parseRevision                     = $parseRevision;
76
        $this->getVersions                       = $getVersions;
77
        $this->pickFromVersion                   = $pickFromVersion;
78
        $this->locateDependencies                = $locateDependencies;
79
        $this->compareApi                        = $compareApi;
80
    }
81
82
    /**
83
     * @throws InvalidArgumentException
84
     */
85
    protected function configure() : void
86
    {
87
        $this
88
            ->setName('roave-backwards-compatibility-check:assert-backwards-compatible')
89
            ->setDescription('Verifies that the revision being compared with "from" does not introduce any BC (backwards-incompatible) changes')
90
            ->addOption(
91
                'from',
92
                null,
93
                InputOption::VALUE_OPTIONAL,
94
                'Git reference for the base version of the library, which is considered "stable"'
95
            )
96
            ->addOption(
97
                'to',
98
                null,
99
                InputOption::VALUE_REQUIRED,
100
                'Git reference for the new version of the library, which is verified against "from" for BC breaks',
101
                'HEAD'
102
            )
103
            ->addOption(
104
                'format',
105
                null,
106
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
107
                'Currently only supports "markdown"'
108
            )
109
            ->addUsage(
110
                <<<'USAGE'
111
112
113
Without arguments, this command will attempt to detect the 
114
latest stable git tag ("release", according to this tool)
115
of the repository in your CWD (current working directory),
116
and will use it as baseline for the defined API.
117
118
It will then create two clones of the repository: one at
119
the "release" version, and one at the version specified
120
via `--to` ("HEAD" by default).
121
122
It will then install all required dependencies in both copies
123
and compare the APIs, looking for breaking changes in the
124
defined "autoload" paths in your `composer.json` definition.
125
126
Once completed, it will print out the results to `STDERR`
127
and terminate with `3` if breaking changes were detected.
128
129
If you want to produce `STDOUT` output, then please use the
130
`--format` flag.
131
USAGE
132
            );
133
    }
134
135
    /**
136
     * @throws InvalidArgumentException
137
     */
138
    public function execute(InputInterface $input, OutputInterface $output) : int
139
    {
140
        assert($output instanceof ConsoleOutputInterface, '');
141
        $stdErr = $output->getErrorOutput();
142
143
        // @todo fix flaky assumption about the path of the source repo...
144
        $sourceRepo = CheckedOutRepository::fromPath(getcwd());
145
146
        $fromRevision = $input->getOption('from') !== null
147
            ? $this->parseRevisionFromInput($input, $sourceRepo)
148
            : $this->determineFromRevisionFromRepository($sourceRepo, $stdErr);
149
150
        $to = $input->getOption('to');
151
152
        assert(is_string($to));
153
154
        $toRevision = $this->parseRevision->fromStringForRepository($to, $sourceRepo);
155
156
        $stdErr->writeln(sprintf('Comparing from %s to %s...', $fromRevision, $toRevision));
157
158
        $fromPath = $this->git->checkout($sourceRepo, $fromRevision);
159
        $toPath   = $this->git->checkout($sourceRepo, $toRevision);
160
161
        try {
162
            $changes = $this->compareApi->__invoke(
163
                $this->makeComposerInstallationReflector->__invoke(
164
                    $fromPath->__toString(),
165
                    new AggregateSourceLocator() // no dependencies
166
                ),
167
                $this->makeComposerInstallationReflector->__invoke(
168
                    $fromPath->__toString(),
169
                    $this->locateDependencies->__invoke($fromPath->__toString())
170
                ),
171
                $this->makeComposerInstallationReflector->__invoke(
172
                    $toPath->__toString(),
173
                    $this->locateDependencies->__invoke($toPath->__toString())
174
                )
175
            );
176
177
            (new SymfonyConsoleTextFormatter($stdErr))->write($changes);
178
179
            $outputFormats = $input->getOption('format') ?: [];
180
181
            assert(is_array($outputFormats));
182
183
            if (ArrayHelpers::stringArrayContainsString('markdown', $outputFormats)) {
184
                (new MarkdownPipedToSymfonyConsoleFormatter($output))->write($changes);
185
            }
186
        } finally {
187
            $this->git->remove($fromPath);
188
            $this->git->remove($toPath);
189
        }
190
191
        return $this->printOutcomeAndExit($changes, $stdErr);
192
    }
193
194
    private function printOutcomeAndExit(Changes $changes, OutputInterface $stdErr) : int
195
    {
196
        $hasBcBreaks = count($changes);
197
198
        if ($hasBcBreaks) {
199
            $stdErr->writeln(sprintf('<error>%s backwards-incompatible changes detected</error>', $hasBcBreaks));
200
        } else {
201
            $stdErr->writeln('<info>No backwards-incompatible changes detected</info>', $hasBcBreaks);
202
        }
203
204
        return $hasBcBreaks ? 3 : 0;
205
    }
206
207
    /**
208
     * @throws InvalidArgumentException
209
     */
210
    private function parseRevisionFromInput(InputInterface $input, CheckedOutRepository $repository) : Revision
211
    {
212
        $from = $input->getOption('from');
213
214
        assert(is_string($from));
215
216
        return $this->parseRevision->fromStringForRepository($from, $repository);
217
    }
218
219
    private function determineFromRevisionFromRepository(
220
        CheckedOutRepository $repository,
221
        OutputInterface $output
222
    ) : Revision {
223
        $versions = $this->getVersions->fromRepository($repository);
224
225
        // @TODO add a test around the 0 limit
226
        Assert::that($versions->count())
227
            ->greaterThan(0, 'Could not detect any released versions for the given repository');
228
229
        $versionString = $this->pickFromVersion->forVersions($versions)->toString();
230
231
        $output->writeln(sprintf('Detected last minor version: %s', $versionString));
232
233
        return $this->parseRevision->fromStringForRepository(
234
            $versionString,
235
            $repository
236
        );
237
    }
238
}
239