Skip to content

Commit 839026b

Browse files
[sync] Show diffs for overwritten resources (#66) (#110)
* [sync] Show diffs for overwritten resources (#66) * Update wiki submodule pointer for PR #110 --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 8e87dd2 commit 839026b

8 files changed

Lines changed: 535 additions & 3 deletions

File tree

.github/wiki

Submodule wiki updated from 486be7d to 8da34fb

docs/commands/copy-resource.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ Options
3636

3737
``--overwrite, -o``
3838
Overwrite existing target files. Without this option, existing files
39-
are skipped.
39+
are skipped. When a text file changes, the command shows a unified diff
40+
before copying. Unchanged targets are reported as skipped, and binary or
41+
unreadable files fall back to a clear non-diff message.
4042

4143
Examples
4244
--------

docs/commands/sync.rst

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ Options
3232
-------
3333

3434
``--overwrite, -o``
35-
Overwrite existing target files.
35+
Overwrite existing target files. Text resources copied through
36+
``copy-resource`` show a readable diff in the sync output before they are
37+
replaced.
3638

3739
Examples
3840
--------
@@ -67,5 +69,7 @@ Behavior
6769

6870
- Updates ``composer.json`` scripts and extra configuration.
6971
- Copies missing workflow stubs, ``.editorconfig``, and ``dependabot.yml``.
72+
- When ``--overwrite`` is enabled, replaced text resources emit a unified diff
73+
so terminal sessions and CI logs show what changed.
7074
- Creates ``.github/wiki`` as a git submodule when missing.
7175
- Calls other commands in sequence.

src/Console/Command/CopyResourceCommand.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
use Composer\Command\BaseCommand;
2323
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
2424
use FastForward\DevTools\Filesystem\FilesystemInterface;
25+
use FastForward\DevTools\Resource\OverwriteDiffRenderer;
2526
use Symfony\Component\Config\FileLocatorInterface;
2627
use Symfony\Component\Console\Attribute\AsCommand;
2728
use Symfony\Component\Console\Input\InputInterface;
@@ -45,11 +46,13 @@ final class CopyResourceCommand extends BaseCommand
4546
* @param FilesystemInterface $filesystem the filesystem used for copy operations
4647
* @param FileLocatorInterface $fileLocator the locator used to resolve source resources
4748
* @param FinderFactoryInterface $finderFactory the factory used to create finders for directory resources
49+
* @param OverwriteDiffRenderer $overwriteDiffRenderer the renderer used to summarize overwrite changes
4850
*/
4951
public function __construct(
5052
private readonly FilesystemInterface $filesystem,
5153
private readonly FileLocatorInterface $fileLocator,
5254
private readonly FinderFactoryInterface $finderFactory,
55+
private readonly OverwriteDiffRenderer $overwriteDiffRenderer,
5356
) {
5457
parent::__construct();
5558
}
@@ -157,6 +160,20 @@ private function copyFile(string $sourcePath, string $targetPath, bool $overwrit
157160
return self::SUCCESS;
158161
}
159162

163+
if ($overwrite && $this->filesystem->exists($targetPath)) {
164+
$comparison = $this->overwriteDiffRenderer->render($sourcePath, $targetPath);
165+
166+
$output->writeln(\sprintf('<comment>%s</comment>', $comparison->summary()));
167+
168+
if ($comparison->isChanged() && null !== $comparison->diff()) {
169+
$output->writeln($comparison->diff());
170+
}
171+
172+
if ($comparison->isUnchanged()) {
173+
return self::SUCCESS;
174+
}
175+
}
176+
160177
$this->filesystem->copy($sourcePath, $targetPath, $overwrite);
161178
$output->writeln(\sprintf('<info>Copied resource %s.</info>', $targetPath));
162179

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
declare(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+
20+
namespace FastForward\DevTools\Resource;
21+
22+
use FastForward\DevTools\Filesystem\FilesystemInterface;
23+
use SebastianBergmann\Diff\Differ;
24+
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
25+
use Throwable;
26+
27+
use function sprintf;
28+
use function str_contains;
29+
use function trim;
30+
31+
/**
32+
* Renders deterministic overwrite summaries and unified diffs for copied files.
33+
*/
34+
final readonly class OverwriteDiffRenderer
35+
{
36+
/**
37+
* Creates a new overwrite diff renderer.
38+
*
39+
* @param FilesystemInterface $filesystem the filesystem used to read compared file contents
40+
*/
41+
public function __construct(private FilesystemInterface $filesystem)
42+
{
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 OverwriteDiffResult the rendered comparison result
52+
*/
53+
public function render(string $sourcePath, string $targetPath): OverwriteDiffResult
54+
{
55+
try {
56+
$sourceContent = $this->filesystem->readFile($sourcePath);
57+
$targetContent = $this->filesystem->readFile($targetPath);
58+
} catch (Throwable) {
59+
return new OverwriteDiffResult(
60+
OverwriteDiffResult::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+
if ($sourceContent === $targetContent) {
70+
return new OverwriteDiffResult(
71+
OverwriteDiffResult::STATUS_UNCHANGED,
72+
sprintf('Target %s already matches source %s; overwrite skipped.', $targetPath, $sourcePath),
73+
);
74+
}
75+
76+
if ($this->isBinary($sourceContent) || $this->isBinary($targetContent)) {
77+
return new OverwriteDiffResult(
78+
OverwriteDiffResult::STATUS_BINARY,
79+
sprintf(
80+
'Target %s will be overwritten from %s, but a text diff is unavailable for binary content.',
81+
$targetPath,
82+
$sourcePath,
83+
),
84+
);
85+
}
86+
87+
$header = sprintf("--- Current: %s\n+++ Source: %s\n", $targetPath, $sourcePath);
88+
$differ = new Differ(new UnifiedDiffOutputBuilder($header));
89+
90+
return new OverwriteDiffResult(
91+
OverwriteDiffResult::STATUS_CHANGED,
92+
sprintf('Overwriting resource %s from %s.', $targetPath, $sourcePath),
93+
trim($differ->diff($targetContent, $sourceContent)),
94+
);
95+
}
96+
97+
/**
98+
* Reports whether the given content should be treated as binary.
99+
*
100+
* @param string $content the content to inspect
101+
*
102+
* @return bool true when the content should not receive a text diff
103+
*/
104+
private function isBinary(string $content): bool
105+
{
106+
return str_contains($content, "\0");
107+
}
108+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(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+
20+
namespace FastForward\DevTools\Resource;
21+
22+
/**
23+
* Carries the result of comparing an overwrite source and target pair.
24+
*/
25+
final readonly class OverwriteDiffResult
26+
{
27+
/**
28+
* @var string indicates that the source and target differ and a text diff is available
29+
*/
30+
public const string STATUS_CHANGED = 'changed';
31+
32+
/**
33+
* @var string indicates that the source and target already match
34+
*/
35+
public const string STATUS_UNCHANGED = 'unchanged';
36+
37+
/**
38+
* @var string indicates that a text diff should not be rendered for the compared files
39+
*/
40+
public const string STATUS_BINARY = 'binary';
41+
42+
/**
43+
* @var string indicates that the compared files could not be read safely
44+
*/
45+
public const string STATUS_UNREADABLE = 'unreadable';
46+
47+
/**
48+
* Creates a new overwrite diff result.
49+
*
50+
* @param string $status the comparison status for the source and target files
51+
* @param string $summary the human-readable summary for console output
52+
* @param string|null $diff the optional unified diff payload
53+
*/
54+
public function __construct(
55+
private string $status,
56+
private string $summary,
57+
private ?string $diff = null,
58+
) {
59+
}
60+
61+
/**
62+
* Returns the comparison status.
63+
*
64+
* @return string the comparison status value
65+
*/
66+
public function status(): string
67+
{
68+
return $this->status;
69+
}
70+
71+
/**
72+
* Returns the human-readable summary.
73+
*
74+
* @return string the summary for console output
75+
*/
76+
public function summary(): string
77+
{
78+
return $this->summary;
79+
}
80+
81+
/**
82+
* Returns the optional unified diff payload.
83+
*
84+
* @return string|null the diff payload, or null when no text diff is available
85+
*/
86+
public function diff(): ?string
87+
{
88+
return $this->diff;
89+
}
90+
91+
/**
92+
* Reports whether the compared files already match.
93+
*
94+
* @return bool true when the source and target contents are identical
95+
*/
96+
public function isUnchanged(): bool
97+
{
98+
return self::STATUS_UNCHANGED === $this->status;
99+
}
100+
101+
/**
102+
* Reports whether the compared files produced a text diff.
103+
*
104+
* @return bool true when a text diff is available
105+
*/
106+
public function isChanged(): bool
107+
{
108+
return self::STATUS_CHANGED === $this->status;
109+
}
110+
}

0 commit comments

Comments
 (0)