Passed
Pull Request — master (#50)
by Marco
02:46
created

AssertBackwardsCompatible::printOutcomeAndExit()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
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\Comparator;
10
use Roave\BackwardCompatibility\Factory\DirectoryReflectorFactory;
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\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\ConsoleOutputInterface;
29
use Symfony\Component\Console\Output\OutputInterface;
30
use function assert;
31
use function count;
32
use function getcwd;
33
use function sprintf;
34
35
final class AssertBackwardsCompatible extends Command
36
{
37
    /** @var PerformCheckoutOfRevision */
38
    private $git;
39
40
    /** @var DirectoryReflectorFactory */
41
    private $reflectorFactory;
42
43
    /** @var ParseRevision */
44
    private $parseRevision;
45
46
    /** @var GetVersionCollection */
47
    private $getVersions;
48
49
    /** @var PickVersionFromVersionCollection */
50
    private $pickFromVersion;
51
52
    /** @var LocateDependencies */
53
    private $locateDependencies;
54
55
    /** @var Comparator */
56
    private $comparator;
57
58
    /**
59
     * @throws LogicException
60
     */
61
    public function __construct(
62
        PerformCheckoutOfRevision $git,
63
        DirectoryReflectorFactory $reflectorFactory,
64
        ParseRevision $parseRevision,
65
        GetVersionCollection $getVersions,
66
        PickVersionFromVersionCollection $pickFromVersion,
67
        LocateDependencies $locateDependencies,
68
        Comparator $comparator
69
    ) {
70
        parent::__construct();
71
72
        $this->git                = $git;
73
        $this->reflectorFactory   = $reflectorFactory;
74
        $this->parseRevision      = $parseRevision;
75
        $this->getVersions        = $getVersions;
76
        $this->pickFromVersion    = $pickFromVersion;
77
        $this->locateDependencies = $locateDependencies;
78
        $this->comparator         = $comparator;
79
    }
80
81
    /**
82
     * @throws InvalidArgumentException
83
     */
84
    protected function configure() : void
85
    {
86
        $this
87
            ->setName('roave-backwards-compatibility-check:assert-backwards-compatible')
88
            ->setDescription('Verifies that the revision being compared with "from" does not introduce any BC (backwards-incompatible) changes')
89
            ->addOption(
90
                'from',
91
                null,
92
                InputOption::VALUE_OPTIONAL,
93
                'Git reference for the base version of the library, which is considered "stable"'
94
            )
95
            ->addOption(
96
                'to',
97
                null,
98
                InputOption::VALUE_REQUIRED,
99
                'Git reference for the new version of the library, which is verified against "from" for BC breaks',
100
                'HEAD'
101
            )
102
            ->addOption(
103
                'format',
104
                null,
105
                InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY,
106
                'Currently only supports "markdown"'
107
            )
108
            ->addArgument(
109
                'sources-path',
110
                InputArgument::OPTIONAL,
111
                'Path to the sources, relative to the repository root',
112
                'src'
113
            )
114
            ->addUsage(
115
                <<<'USAGE'
116
117
118
Without arguments, this command will attempt to detect the 
119
latest stable git tag ("release", according to this tool)
120
of the repository in your CWD (current working directory),
121
and will use it as baseline for the defined API.
122
123
It will then create two clones of the repository: one at
124
the "release" version, and one at the version specified
125
via `--to` ("HEAD" by default).
126
127
It will then install all required dependencies in both copies
128
and compare the APIs, looking for breaking changes in the
129
given `<sources-path>` ("src" by default).
130
131
Once completed, it will print out the results to `STDERR`
132
and terminate with `1` if breaking changes were detected.
133
134
If you want to produce `STDOUT` output, then please use the
135
`--format` flag.
136
USAGE
137
            );
138
    }
139
140
    /**
141
     * @throws InvalidArgumentException
142
     */
143
    public function execute(InputInterface $input, OutputInterface $output) : int
144
    {
145
        assert($output instanceof ConsoleOutputInterface, '');
146
        $stdErr = $output->getErrorOutput();
0 ignored issues
show
Bug introduced by
The method getErrorOutput() does not exist on Symfony\Component\Console\Output\OutputInterface. It seems like you code against a sub-type of Symfony\Component\Console\Output\OutputInterface such as Symfony\Component\Consol...\ConsoleOutputInterface or Symfony\Component\Console\Style\OutputStyle or Symfony\Component\Console\Output\ConsoleOutput. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

146
        /** @scrutinizer ignore-call */ 
147
        $stdErr = $output->getErrorOutput();
Loading history...
147
148
        // @todo fix flaky assumption about the path of the source repo...
149
        $sourceRepo = CheckedOutRepository::fromPath(getcwd());
150
151
        $fromRevision = $input->hasOption('from')
152
            ? $this->parseRevisionFromInput($input, $sourceRepo)
153
            : $this->determineFromRevisionFromRepository($sourceRepo, $stdErr);
154
155
        $toRevision  = $this->parseRevision->fromStringForRepository($input->getOption('to'), $sourceRepo);
156
        $sourcesPath = $input->getArgument('sources-path');
157
158
        $stdErr->writeln(sprintf('Comparing from %s to %s...', (string) $fromRevision, (string) $toRevision));
159
160
        $fromPath = $this->git->checkout($sourceRepo, $fromRevision);
161
        $toPath   = $this->git->checkout($sourceRepo, $toRevision);
162
163
        try {
164
            $fromSources = $fromPath . '/' . $sourcesPath;
165
            $toSources   = $toPath . '/' . $sourcesPath;
166
167
            Assert::that($fromSources)->directory();
168
            Assert::that($toSources)->directory();
169
170
            $changes = $this->comparator->compare(
171
                $this->reflectorFactory->__invoke(
172
                    $fromPath . '/' . $sourcesPath,
173
                    new AggregateSourceLocator() // no dependencies
174
                ),
175
                $this->reflectorFactory->__invoke(
176
                    $fromPath . '/' . $sourcesPath,
177
                    $this->locateDependencies->__invoke((string) $fromPath)
178
                ),
179
                $this->reflectorFactory->__invoke(
180
                    $toPath . '/' . $sourcesPath,
181
                    $this->locateDependencies->__invoke((string) $toPath)
182
                )
183
            );
184
185
            (new SymfonyConsoleTextFormatter($stdErr))->write($changes);
186
187
            $outputFormats = $input->getOption('format') ?: [];
188
            Assert::that($outputFormats)->isArray();
189
190
            if (ArrayHelpers::stringArrayContainsString('markdown', $outputFormats)) {
191
                (new MarkdownPipedToSymfonyConsoleFormatter($output))->write($changes);
192
            }
193
        } finally {
194
            $this->git->remove($fromPath);
195
            $this->git->remove($toPath);
196
        }
197
198
        return $this->printOutcomeAndExit($changes, $stdErr);
199
    }
200
201
    private function printOutcomeAndExit(Changes $changes, OutputInterface $stdErr) : int
202
    {
203
        $hasBcBreaks = count($changes);
204
205
        if ($hasBcBreaks) {
206
            $stdErr->writeln(sprintf('<error>%s backwards-incompatible changes detected</error>', $hasBcBreaks));
207
        } else {
208
            $stdErr->writeln('<info>No backwards-incompatible changes detected</info>', $hasBcBreaks);
209
        }
210
211
        return (int) (bool) $hasBcBreaks;
212
    }
213
214
    /**
215
     * @throws InvalidArgumentException
216
     */
217
    private function parseRevisionFromInput(InputInterface $input, CheckedOutRepository $repository) : Revision
218
    {
219
        return $this->parseRevision->fromStringForRepository(
220
            (string) $input->getOption('from'),
221
            $repository
222
        );
223
    }
224
225
    private function determineFromRevisionFromRepository(
226
        CheckedOutRepository $repository,
227
        OutputInterface $output
228
    ) : Revision {
229
        $versionString = $this->pickFromVersion->forVersions(
230
            $this->getVersions->fromRepository($repository)
231
        )->getVersionString();
232
        $output->writeln(sprintf('Detected last minor version: %s', $versionString));
233
        return $this->parseRevision->fromStringForRepository(
234
            $versionString,
235
            $repository
236
        );
237
    }
238
}
239