Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
FileDiffer
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
6 / 6
20
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 diff
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 diffContents
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
1 / 1
6
 colorize
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 formatForConsole
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isBinary
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/**
6 * Fast Forward Development Tools for PHP projects.
7 *
8 * This file is part of fast-forward/dev-tools project.
9 *
10 * @author   Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
11 * @license  https://opensource.org/licenses/MIT MIT License
12 *
13 * @see      https://github.com/php-fast-forward/
14 * @see      https://github.com/php-fast-forward/dev-tools
15 * @see      https://github.com/php-fast-forward/dev-tools/issues
16 * @see      https://php-fast-forward.github.io/dev-tools/
17 * @see      https://datatracker.ietf.org/doc/html/rfc2119
18 */
19
20namespace FastForward\DevTools\Resource;
21
22use FastForward\DevTools\Filesystem\FilesystemInterface;
23use Throwable;
24use function explode;
25use function implode;
26use function str_contains;
27use function str_starts_with;
28
29/**
30 * Renders deterministic summaries and unified diffs for file comparisons.
31 */
32  class FileDiffer
33{
34    /**
35     * Creates a new file differ.
36     *
37     * @param FilesystemInterface $filesystem the filesystem used to read compared file contents
38     * @param DifferInterface $differ the differ used to generate unified diffs
39     */
40    public function __construct(
41        private FilesystemInterface $filesystem,
42        private DifferInterface $differ,
43    ) {}
44
45    /**
46     * Compares a source file against the target file that would be overwritten.
47     *
48     * @param string $sourcePath the source file path that would replace the target
49     * @param string $targetPath the existing target file path
50     *
51     * @return FileDiff the rendered comparison result
52     */
53    public function diff(string $sourcePath, string $targetPath): FileDiff
54    {
55        try {
56            $sourceContent = $this->filesystem->readFile($sourcePath);
57            $targetContent = $this->filesystem->readFile($targetPath);
58        } catch (Throwable) {
59            return new FileDiff(
60                FileDiff::STATUS_UNREADABLE,
61                \sprintf(
62                    'Target %s will be overwritten from %s, but the existing or source content could not be read.',
63                    $targetPath,
64                    $sourcePath,
65                ),
66            );
67        }
68
69        return $this->diffContents(
70            $sourcePath,
71            $targetPath,
72            $sourceContent,
73            $targetContent,
74            \sprintf('Overwriting resource %s from %s.', $targetPath, $sourcePath),
75        );
76    }
77
78    /**
79     * Compares managed content against the current target contents.
80     *
81     * @param string $sourceLabel the human-readable source label shown in summaries
82     * @param string $targetPath the target file path
83     * @param string $sourceContent the generated or source content
84     * @param string|null $targetContent the current target content, or null when the target does not exist
85     * @param string|null $changedSummary an optional changed-state summary override
86     *
87     * @return FileDiff the rendered comparison result
88     */
89    public function diffContents(
90        string $sourceLabel,
91        string $targetPath,
92        string $sourceContent,
93        ?string $targetContent,
94        ?string $changedSummary = null,
95    ): FileDiff {
96        if (null !== $targetContent && $sourceContent === $targetContent) {
97            return new FileDiff(
98                FileDiff::STATUS_UNCHANGED,
99                \sprintf('Target %s already matches source %s; overwrite skipped.', $targetPath, $sourceLabel),
100            );
101        }
102
103        if ($this->isBinary($sourceContent) || (null !== $targetContent && $this->isBinary($targetContent))) {
104            return new FileDiff(
105                FileDiff::STATUS_BINARY,
106                \sprintf(
107                    'Target %s will be overwritten from %s, but a text diff is unavailable for binary content.',
108                    $targetPath,
109                    $sourceLabel,
110                ),
111            );
112        }
113
114        $targetContent ??= '';
115        $changedSummary ??= \sprintf('Overwriting resource %s from %s.', $targetPath, $sourceLabel);
116
117        return new FileDiff(
118            FileDiff::STATUS_CHANGED,
119            $changedSummary,
120            $this->differ->diff($targetContent, $sourceContent),
121        );
122    }
123
124    /**
125     * Colorizes a unified diff for decorated console output.
126     *
127     * @param string $diff the plain unified diff
128     *
129     * @return string the colorized diff using Symfony Console tags
130     */
131    public function colorize(string $diff): string
132    {
133        $lines = explode("\n", $diff);
134
135        foreach ($lines as &$line) {
136            if (str_starts_with($line, '+++') || str_starts_with($line, '---')) {
137                $line = \sprintf('<fg=cyan>%s</>', $line);
138
139                continue;
140            }
141
142            if (str_starts_with($line, '@@')) {
143                $line = \sprintf('<fg=yellow>%s</>', $line);
144
145                continue;
146            }
147
148            if (str_starts_with($line, '+')) {
149                $line = \sprintf('<fg=green>%s</>', $line);
150
151                continue;
152            }
153
154            if (str_starts_with($line, '-')) {
155                $line = \sprintf('<fg=red>%s</>', $line);
156            }
157        }
158
159        return implode("\n", $lines);
160    }
161
162    /**
163     * Formats a diff payload for console output.
164     *
165     * @param string|null $diff the plain unified diff, if available
166     * @param bool $decorated whether console decoration is enabled
167     *
168     * @return string|null the diff payload ready for console output
169     */
170    public function formatForConsole(?string $diff, bool $decorated): ?string
171    {
172        if (null === $diff) {
173            return null;
174        }
175
176        if (! $decorated) {
177            return $diff;
178        }
179
180        return $this->colorize($diff);
181    }
182
183    /**
184     * Reports whether the given content should be treated as binary.
185     *
186     * @param string $content the content to inspect
187     *
188     * @return bool true when the content should not receive a text diff
189     */
190    private function isBinary(string $content): bool
191    {
192        return str_contains($content, "\0");
193    }
194}