Passed
Pull Request — master (#376)
by Théo
02:28
created

Diff::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box\Console\Command;
16
17
use function array_filter;
18
use function array_flip;
19
use Assert\Assertion;
20
use function count;
21
use function is_string;
22
use KevinGH\Box\Console\PharInfoRenderer;
23
use function KevinGH\Box\format_size;
24
use function KevinGH\Box\get_phar_compression_algorithms;
25
use KevinGH\Box\PharInfo\PharDiff;
26
use KevinGH\Box\PharInfo\PharInfo;
27
use KevinGH\Box\PhpSettingsHandler;
28
use PharFileInfo;
29
use function sprintf;
30
use Symfony\Component\Console\Input\InputArgument;
31
use Symfony\Component\Console\Input\InputInterface;
32
use Symfony\Component\Console\Input\InputOption;
33
use Symfony\Component\Console\Logger\ConsoleLogger;
34
use Symfony\Component\Console\Output\OutputInterface;
35
use Symfony\Component\Console\Style\SymfonyStyle;
36
use Throwable;
37
38
/**
39
 * @private
40
 */
41
final class Diff extends Command
42
{
43
    use CreateTemporaryPharFile;
44
45
    private const FIRST_PHAR_ARG = 'pharA';
46
    private const SECOND_PHAR_ARG = 'pharB';
47
48
    private const LIST_FILES_DIFF_OPTION = 'list-diff';
49
    private const GIT_DIFF_OPTION = 'git-diff';
50
    private const GNU_DIFF_OPTION = 'gnu-diff';
51
    private const CHECK_OPTION = 'check';
52
53
    private static $FILE_ALGORITHMS;
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function __construct(?string $name = null)
59
    {
60
        parent::__construct($name);
61
62
        if (null === self::$FILE_ALGORITHMS) {
63
            self::$FILE_ALGORITHMS = array_flip(array_filter(get_phar_compression_algorithms()));
64
        }
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    protected function configure(): void
71
    {
72
        parent::configure();
73
74
        $this->setName('diff');
75
        $this->setDescription('🕵  Displays the differences between all of the files in two PHARs');
76
77
        $this->addArgument(
78
            self::FIRST_PHAR_ARG,
79
            InputArgument::REQUIRED,
80
            'The first PHAR'
81
        );
82
        $this->addArgument(
83
            self::SECOND_PHAR_ARG,
84
            InputArgument::REQUIRED,
85
            'The second PHAR'
86
        );
87
88
        $this->addOption(
89
            self::GNU_DIFF_OPTION,
90
            null,
91
            InputOption::VALUE_NONE,
92
            'Displays a GNU diff'
93
        );
94
        $this->addOption(
95
            self::GIT_DIFF_OPTION,
96
            null,
97
            InputOption::VALUE_NONE,
98
            'Displays a Git diff'
99
        );
100
        $this->addOption(
101
            self::LIST_FILES_DIFF_OPTION,
102
            null,
103
            InputOption::VALUE_NONE,
104
            'Displays a list of file names diff (default)'
105
        );
106
        $this->addOption(
107
            self::CHECK_OPTION,
108
            'c',
109
            InputOption::VALUE_OPTIONAL,
110
            'Verify the authenticity of the contents between the two PHARs with the given hash function.',
111
            'sha384'
112
        );
113
    }
114
115
    /**
116
     * {@inheritdoc}
117
     */
118
    protected function execute(InputInterface $input, OutputInterface $output): int
119
    {
120
        $io = new SymfonyStyle($input, $output);
121
122
        (new PhpSettingsHandler(new ConsoleLogger($output)))->check();
123
124
        $paths = [
125
            $input->getArgument(self::FIRST_PHAR_ARG),
126
            $input->getArgument(self::SECOND_PHAR_ARG),
127
        ];
128
129
        Assertion::allFile($paths);
0 ignored issues
show
Bug introduced by
$paths of type array<integer,null|string|string[]> is incompatible with the type string expected by parameter $value of Assert\Assertion::allFile(). ( Ignorable by Annotation )

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

129
        Assertion::allFile(/** @scrutinizer ignore-type */ $paths);
Loading history...
130
131
        try {
132
            $diff = new PharDiff(...$paths);
133
        } catch (Throwable $throwable) {
134
            if ($output->isDebug()) {
135
                throw $throwable;
136
            }
137
138
            $io->writeln(
139
                sprintf(
140
                    '<error>Could not check the PHARs: %s</error>',
141
                    $throwable->getMessage()
142
                )
143
            );
144
145
            return 1;
146
        }
147
148
        $result1 = $this->compareArchives($diff, $io);
149
        $result2 = $this->compareContents($input, $diff, $io);
150
151
        return $result1 + $result2;
152
    }
153
154
    private function compareArchives(PharDiff $diff, SymfonyStyle $io): int
155
    {
156
        $io->comment('<info>Comparing the two archives... (do not check the signatures)</info>');
157
158
        $pharInfoA = $diff->getPharA()->getPharInfo();
159
        $pharInfoB = $diff->getPharB()->getPharInfo();
160
161
        if ($pharInfoA->equals($pharInfoB)) {
162
            $io->success('The two archives are identical');
163
164
            return 0;
165
        }
166
167
        $this->renderArchive(
168
            $diff->getPharA()->getFileName(),
169
            $pharInfoA,
170
            $io
171
        );
172
173
        $io->newLine();
174
175
        $this->renderArchive(
176
            $diff->getPharB()->getFileName(),
177
            $pharInfoA,
178
            $io
179
        );
180
181
        return 1;
182
    }
183
184
    private function compareContents(InputInterface $input, PharDiff $diff, SymfonyStyle $io): int
185
    {
186
        $io->comment('<info>Comparing the two archives contents...</info>');
187
188
        if ($input->hasParameterOption(['-c', '--check'])) {
189
            return $diff->listChecksums($input->getOption(self::CHECK_OPTION) ?? 'sha384');
190
        }
191
192
        if ($input->getOption(self::GNU_DIFF_OPTION)) {
193
            $diffResult = $diff->gnuDiff();
194
        } elseif ($input->getOption(self::GIT_DIFF_OPTION)) {
195
            $diffResult = $diff->gitDiff();
196
        } else {
197
            $diffResult = $diff->listDiff($diff, $io);
0 ignored issues
show
Unused Code introduced by
The call to KevinGH\Box\PharInfo\PharDiff::listDiff() has too many arguments starting with $diff. ( Ignorable by Annotation )

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

197
            /** @scrutinizer ignore-call */ 
198
            $diffResult = $diff->listDiff($diff, $io);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
198
        }
199
200
        if (null === $diffResult || [[], []] === $diffResult) {
201
            $io->success('The contents are identical');
202
203
            return 0;
204
        }
205
206
        if (is_string($diffResult)) {
207
            // Git or GNU diff: we don't have much control on the format
208
            $io->writeln($diffResult);
209
210
            return 1;
211
        }
212
213
        $io->writeln(sprintf(
214
            '--- Files present in "%s" but not in "%s"',
215
            $diff->getPharA()->getFileName(),
216
            $diff->getPharB()->getFileName()
217
        ));
218
        $io->writeln(sprintf(
219
            '+++ Files present in "%s" but not in "%s"',
220
            $diff->getPharB()->getFileName(),
221
            $diff->getPharA()->getFileName()
222
        ));
223
224
        $io->newLine();
225
226
        $renderPaths = static function (string $symbol, PharInfo $pharInfo, array $paths, SymfonyStyle $io): void {
227
            foreach ($paths as $path) {
228
                /** @var PharFileInfo $file */
229
                $file = $pharInfo->getPhar()[str_replace($pharInfo->getRoot(), '', $path)];
230
231
                $compression = '<fg=red>[NONE]</fg=red>';
232
233
                foreach (self::$FILE_ALGORITHMS as $code => $name) {
234
                    if ($file->isCompressed($code)) {
235
                        $compression = "<fg=cyan>[$name]</fg=cyan>";
236
                        break;
237
                    }
238
                }
239
240
                $fileSize = format_size($file->getCompressedSize());
241
242
                $io->writeln(
243
                    sprintf(
244
                        '%s %s %s - %s',
245
                        $symbol,
246
                        $path,
247
                        $compression,
248
                        $fileSize
249
                    )
250
                );
251
            }
252
        };
253
254
        $renderPaths('-', $diff->getPharA()->getPharInfo(), $diffResult[0], $io);
255
        $renderPaths('+', $diff->getPharB()->getPharInfo(), $diffResult[1], $io);
256
257
        $io->error(sprintf(
258
            '%d file(s) difference',
259
            count($diffResult[0]) + count($diffResult[1])
260
        ));
261
262
        return 1;
263
    }
264
265
    private function renderArchive(string $fileName, PharInfo $pharInfo, SymfonyStyle $io): void
266
    {
267
        $io->writeln(
268
            sprintf(
269
                '<comment>Archive: </comment><fg=cyan;options=bold>%s</>',
270
                $fileName
271
            )
272
        );
273
274
        PharInfoRenderer::renderCompression($pharInfo, $io);
275
        // Omit the signature
276
        PharInfoRenderer::renderMetadata($pharInfo, $io);
277
        PharInfoRenderer::renderContentsSummary($pharInfo, $io);
278
    }
279
}
280