Issues (224)

src/Console/Command/Diff.php (2 issues)

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 Fidry\Console\Command\Command;
18
use Fidry\Console\Command\Configuration;
19
use Fidry\Console\ExitCode;
20
use Fidry\Console\IO;
0 ignored issues
show
The type Fidry\Console\IO was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use KevinGH\Box\Console\PharInfoRenderer;
22
use KevinGH\Box\Phar\DiffMode;
23
use KevinGH\Box\Phar\PharDiff;
24
use KevinGH\Box\Phar\PharInfo;
25
use SebastianBergmann\Diff\Differ;
26
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
27
use Symfony\Component\Console\Exception\RuntimeException;
28
use Symfony\Component\Console\Input\InputArgument;
29
use Symfony\Component\Console\Input\InputOption;
30
use Symfony\Component\Console\Output\BufferedOutput;
31
use Symfony\Component\Filesystem\Path;
32
use ValueError;
33
use Webmozart\Assert\Assert;
34
use function array_map;
35
use function explode;
36
use function implode;
37
use function sprintf;
38
use function str_starts_with;
39
40
/**
41
 * @private
42
 */
43
final class Diff implements Command
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 DIFF_OPTION = 'diff';
52
    private const CHECK_OPTION = 'check';
53
    private const CHECKSUM_ALGORITHM_OPTION = 'checksum-algorithm';
54
55
    private const DEFAULT_CHECKSUM_ALGO = 'sha384';
56
57
    public function getConfiguration(): Configuration
58
    {
59
        return new Configuration(
60
            'diff',
61
            '🕵  Displays the differences between all of the files in two PHARs',
62
            '',
63
            [
64
                new InputArgument(
65
                    self::FIRST_PHAR_ARG,
66
                    InputArgument::REQUIRED,
67
                    'The first PHAR',
68
                ),
69
                new InputArgument(
70
                    self::SECOND_PHAR_ARG,
71
                    InputArgument::REQUIRED,
72
                    'The second PHAR',
73
                ),
74
            ],
75
            [
76
                new InputOption(
77
                    self::GNU_DIFF_OPTION,
78
                    null,
79
                    InputOption::VALUE_NONE,
80
                    '(deprecated) Displays a GNU diff',
81
                ),
82
                new InputOption(
83
                    self::GIT_DIFF_OPTION,
84
                    null,
85
                    InputOption::VALUE_NONE,
86
                    '(deprecated) Displays a Git diff',
87
                ),
88
                new InputOption(
89
                    self::LIST_FILES_DIFF_OPTION,
90
                    null,
91
                    InputOption::VALUE_NONE,
92
                    '(deprecated) Displays a list of file names diff (default)',
93
                ),
94
                new InputOption(
95
                    self::DIFF_OPTION,
96
                    null,
97
                    InputOption::VALUE_REQUIRED,
98
                    sprintf(
99
                        'Displays a diff of the files. Available options are: "%s"',
100
                        implode(
101
                            '", "',
102
                            DiffMode::values(),
103
                        ),
104
                    ),
105
                    DiffMode::CHECKSUM->value,
106
                ),
107
                new InputOption(
108
                    self::CHECK_OPTION,
109
                    'c',
110
                    InputOption::VALUE_OPTIONAL,
111
                    '(deprecated) Verify the authenticity of the contents between the two PHARs with the given hash function',
112
                ),
113
                new InputOption(
114
                    self::CHECKSUM_ALGORITHM_OPTION,
115
                    null,
116
                    InputOption::VALUE_REQUIRED,
117
                    sprintf(
118
                        'The hash function used to compare files with the diff mode used is "%s".',
119
                        DiffMode::CHECKSUM->value,
120
                    ),
121
                    self::DEFAULT_CHECKSUM_ALGO,
122
                ),
123
            ],
124
        );
125
    }
126
127
    public function execute(IO $io): int
128
    {
129
        $diff = new PharDiff(...self::getPaths($io));
130
        $diffMode = self::getDiffMode($io);
131
        $checksumAlgorithm = self::getChecksumAlgorithm($io);
132
133
        $io->comment('<info>Comparing the two archives...</info>');
134
135
        if ($diff->equals()) {
136
            $io->success('The two archives are identical.');
137
138
            return ExitCode::SUCCESS;
139
        }
140
141
        self::renderSummary($diff->getPharInfoA(), $io);
142
        $io->newLine();
143
        self::renderSummary($diff->getPharInfoB(), $io);
144
145
        $this->renderArchivesDiff($diff, $io);
146
        $this->renderContentsDiff($diff, $diffMode, $checksumAlgorithm, $io);
147
148
        return ExitCode::FAILURE;
149
    }
150
151
    /**
152
     * @return array{non-empty-string, non-empty-string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{non-empty-string, non-empty-string} at position 2 could not be parsed: Expected ':' at position 2, but found 'non-empty-string'.
Loading history...
153
     */
154
    private static function getPaths(IO $io): array
155
    {
156
        $paths = [
157
            $io->getTypedArgument(self::FIRST_PHAR_ARG)->asNonEmptyString(),
158
            $io->getTypedArgument(self::SECOND_PHAR_ARG)->asNonEmptyString(),
159
        ];
160
161
        Assert::allFile($paths);
162
163
        return array_map(
164
            static fn (string $path) => Path::canonicalize($path),
165
            $paths,
166
        );
167
    }
168
169
    private function renderArchivesDiff(PharDiff $diff, IO $io): void
170
    {
171
        $pharASummary = self::getShortSummary($diff->getPharInfoA(), $io);
172
        $pharBSummary = self::getShortSummary($diff->getPharInfoB(), $io);
173
174
        if ($pharASummary === $pharBSummary) {
175
            return;
176
        }
177
178
        $io->writeln(
179
            self::createColorizedDiff(
180
                $pharASummary,
181
                $pharBSummary,
182
            ),
183
        );
184
    }
185
186
    private static function createColorizedDiff(string $pharASummary, string $pharBSummary): string
187
    {
188
        $differ = new Differ(
189
            new UnifiedDiffOutputBuilder(
190
                "\n<diff-expected>--- PHAR A</diff-expected>\n<diff-actual>+++ PHAR B</diff-actual>\n",
191
            ),
192
        );
193
194
        $result = $differ->diff(
195
            $pharASummary,
196
            $pharBSummary,
197
        );
198
199
        $lines = explode("\n", $result);
200
201
        $colorizedLines = array_map(
202
            static fn (string $line) => match (true) {
203
                str_starts_with($line, '+') => sprintf(
204
                    '<diff-actual>%s</diff-actual>',
205
                    $line,
206
                ),
207
                str_starts_with($line, '-') => sprintf(
208
                    '<diff-expected>%s</diff-expected>',
209
                    $line,
210
                ),
211
                default => $line,
212
            },
213
            $lines,
214
        );
215
216
        return implode("\n", $colorizedLines);
217
    }
218
219
    private static function getDiffMode(IO $io): DiffMode
220
    {
221
        if ($io->getTypedOption(self::GNU_DIFF_OPTION)->asBoolean()) {
222
            $io->writeln(
223
                sprintf(
224
                    '⚠️  <warning>Using the option "%s" is deprecated. Use "--%s=%s" instead.</warning>',
225
                    self::GNU_DIFF_OPTION,
226
                    self::DIFF_OPTION,
227
                    DiffMode::GNU->value,
228
                ),
229
            );
230
231
            return DiffMode::GNU;
232
        }
233
234
        if ($io->getTypedOption(self::GIT_DIFF_OPTION)->asBoolean()) {
235
            $io->writeln(
236
                sprintf(
237
                    '⚠️  <warning>Using the option "%s" is deprecated. Use "--%s=%s" instead.</warning>',
238
                    self::GIT_DIFF_OPTION,
239
                    self::DIFF_OPTION,
240
                    DiffMode::GIT->value,
241
                ),
242
            );
243
244
            return DiffMode::GIT;
245
        }
246
247
        if ($io->getTypedOption(self::LIST_FILES_DIFF_OPTION)->asBoolean()) {
248
            $io->writeln(
249
                sprintf(
250
                    '⚠️  <warning>Using the option "%s" is deprecated. Use "--%s=%s" instead.</warning>',
251
                    self::LIST_FILES_DIFF_OPTION,
252
                    self::DIFF_OPTION,
253
                    DiffMode::FILE_NAME->value,
254
                ),
255
            );
256
257
            return DiffMode::FILE_NAME;
258
        }
259
260
        if ($io->hasOption('-c') || $io->hasOption('--check')) {
261
            $io->writeln(
262
                sprintf(
263
                    '⚠️  <warning>Using the option "%s" is deprecated. Use "--%s=%s" instead.</warning>',
264
                    self::CHECK_OPTION,
265
                    self::DIFF_OPTION,
266
                    DiffMode::CHECKSUM->value,
267
                ),
268
            );
269
270
            return DiffMode::FILE_NAME;
271
        }
272
273
        $rawDiffOption = $io->getTypedOption(self::DIFF_OPTION)->asNonEmptyString();
274
275
        try {
276
            return DiffMode::from($rawDiffOption);
277
        } catch (ValueError) {
278
            // Rethrow a more user-friendly error message
279
            throw new RuntimeException(
280
                sprintf(
281
                    'Invalid diff mode "%s". Expected one of: "%s".',
282
                    $rawDiffOption,
283
                    implode(
284
                        '", "',
285
                        DiffMode::values(),
286
                    ),
287
                ),
288
            );
289
        }
290
    }
291
292
    private static function getChecksumAlgorithm(IO $io): string
293
    {
294
        $checksumAlgorithm = $io->getTypedOption(self::CHECK_OPTION)->asNullableNonEmptyString();
295
296
        if (null !== $checksumAlgorithm) {
297
            $io->writeln(
298
                sprintf(
299
                    '⚠️  <warning>Using the option "%s" is deprecated. Use "--%s=\<algorithm\>" instead.</warning>',
300
                    self::CHECK_OPTION,
301
                    self::CHECKSUM_ALGORITHM_OPTION,
302
                ),
303
            );
304
305
            return $checksumAlgorithm;
306
        }
307
308
        return $io->getTypedOption(self::CHECKSUM_ALGORITHM_OPTION)->asNullableNonEmptyString() ?? self::DEFAULT_CHECKSUM_ALGO;
309
    }
310
311
    private function renderContentsDiff(PharDiff $diff, DiffMode $diffMode, string $checksumAlgorithm, IO $io): void
312
    {
313
        $io->comment(
314
            sprintf(
315
                '<info>Comparing the two archives contents (%s diff)...</info>',
316
                $diffMode->value,
317
            ),
318
        );
319
320
        $diff->diff($diffMode, $checksumAlgorithm, $io);
321
    }
322
323
    private static function renderSummary(PharInfo $pharInfo, IO $io): void
324
    {
325
        $io->writeln(
326
            sprintf(
327
                '<comment>Archive: </comment><fg=cyan;options=bold>%s</>',
328
                $pharInfo->getFileName(),
329
            ),
330
        );
331
332
        PharInfoRenderer::renderShortSummary($pharInfo, $io);
333
    }
334
335
    private static function getShortSummary(PharInfo $pharInfo, IO $io): string
336
    {
337
        $output = new BufferedOutput(
338
            $io->getVerbosity(),
339
            false,
340
            clone $io->getOutput()->getFormatter(),
341
        );
342
343
        PharInfoRenderer::renderShortSummary(
344
            $pharInfo,
345
            $io->withOutput($output),
346
        );
347
348
        return $output->fetch();
349
    }
350
}
351