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