Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/wiki
Submodule wiki updated from 486be7 to 8da34f
4 changes: 3 additions & 1 deletion docs/commands/copy-resource.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ Options

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

Examples
--------
Expand Down
6 changes: 5 additions & 1 deletion docs/commands/sync.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ Options
-------

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

Examples
--------
Expand Down Expand Up @@ -67,5 +69,7 @@ Behavior

- Updates ``composer.json`` scripts and extra configuration.
- Copies missing workflow stubs, ``.editorconfig``, and ``dependabot.yml``.
- When ``--overwrite`` is enabled, replaced text resources emit a unified diff
so terminal sessions and CI logs show what changed.
- Creates ``.github/wiki`` as a git submodule when missing.
- Calls other commands in sequence.
17 changes: 17 additions & 0 deletions src/Console/Command/CopyResourceCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use Composer\Command\BaseCommand;
use FastForward\DevTools\Filesystem\FinderFactoryInterface;
use FastForward\DevTools\Filesystem\FilesystemInterface;
use FastForward\DevTools\Resource\OverwriteDiffRenderer;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -45,11 +46,13 @@ final class CopyResourceCommand extends BaseCommand
* @param FilesystemInterface $filesystem the filesystem used for copy operations
* @param FileLocatorInterface $fileLocator the locator used to resolve source resources
* @param FinderFactoryInterface $finderFactory the factory used to create finders for directory resources
* @param OverwriteDiffRenderer $overwriteDiffRenderer the renderer used to summarize overwrite changes
*/
public function __construct(
private readonly FilesystemInterface $filesystem,
private readonly FileLocatorInterface $fileLocator,
private readonly FinderFactoryInterface $finderFactory,
private readonly OverwriteDiffRenderer $overwriteDiffRenderer,
) {
parent::__construct();
}
Expand Down Expand Up @@ -157,6 +160,20 @@ private function copyFile(string $sourcePath, string $targetPath, bool $overwrit
return self::SUCCESS;
}

if ($overwrite && $this->filesystem->exists($targetPath)) {
$comparison = $this->overwriteDiffRenderer->render($sourcePath, $targetPath);

$output->writeln(\sprintf('<comment>%s</comment>', $comparison->summary()));

if ($comparison->isChanged() && null !== $comparison->diff()) {
$output->writeln($comparison->diff());
}

if ($comparison->isUnchanged()) {
return self::SUCCESS;
}
}

$this->filesystem->copy($sourcePath, $targetPath, $overwrite);
$output->writeln(\sprintf('<info>Copied resource %s.</info>', $targetPath));

Expand Down
108 changes: 108 additions & 0 deletions src/Resource/OverwriteDiffRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

declare(strict_types=1);

/**
* Fast Forward Development Tools for PHP projects.
*
* This file is part of fast-forward/dev-tools project.
*
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*
* @see https://github.com/php-fast-forward/
* @see https://github.com/php-fast-forward/dev-tools
* @see https://github.com/php-fast-forward/dev-tools/issues
* @see https://php-fast-forward.github.io/dev-tools/
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/

namespace FastForward\DevTools\Resource;

use FastForward\DevTools\Filesystem\FilesystemInterface;
use SebastianBergmann\Diff\Differ;
use SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder;
use Throwable;

use function sprintf;
use function str_contains;
use function trim;

/**
* Renders deterministic overwrite summaries and unified diffs for copied files.
*/
final readonly class OverwriteDiffRenderer
{
/**
* Creates a new overwrite diff renderer.
*
* @param FilesystemInterface $filesystem the filesystem used to read compared file contents
*/
public function __construct(private FilesystemInterface $filesystem)
{
}

/**
* Compares a source file against the target file that would be overwritten.
*
* @param string $sourcePath the source file path that would replace the target
* @param string $targetPath the existing target file path
*
* @return OverwriteDiffResult the rendered comparison result
*/
public function render(string $sourcePath, string $targetPath): OverwriteDiffResult
{
try {
$sourceContent = $this->filesystem->readFile($sourcePath);
$targetContent = $this->filesystem->readFile($targetPath);
} catch (Throwable) {
return new OverwriteDiffResult(
OverwriteDiffResult::STATUS_UNREADABLE,
sprintf(
'Target %s will be overwritten from %s, but the existing or source content could not be read.',
$targetPath,
$sourcePath,
),
);
}

if ($sourceContent === $targetContent) {
return new OverwriteDiffResult(
OverwriteDiffResult::STATUS_UNCHANGED,
sprintf('Target %s already matches source %s; overwrite skipped.', $targetPath, $sourcePath),
);
}

if ($this->isBinary($sourceContent) || $this->isBinary($targetContent)) {
return new OverwriteDiffResult(
OverwriteDiffResult::STATUS_BINARY,
sprintf(
'Target %s will be overwritten from %s, but a text diff is unavailable for binary content.',
$targetPath,
$sourcePath,
),
);
}

$header = sprintf("--- Current: %s\n+++ Source: %s\n", $targetPath, $sourcePath);
$differ = new Differ(new UnifiedDiffOutputBuilder($header));

return new OverwriteDiffResult(
OverwriteDiffResult::STATUS_CHANGED,
sprintf('Overwriting resource %s from %s.', $targetPath, $sourcePath),
trim($differ->diff($targetContent, $sourceContent)),
);
}

/**
* Reports whether the given content should be treated as binary.
*
* @param string $content the content to inspect
*
* @return bool true when the content should not receive a text diff
*/
private function isBinary(string $content): bool
{
return str_contains($content, "\0");
}
}
110 changes: 110 additions & 0 deletions src/Resource/OverwriteDiffResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

/**
* Fast Forward Development Tools for PHP projects.
*
* This file is part of fast-forward/dev-tools project.
*
* @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com>
* @license https://opensource.org/licenses/MIT MIT License
*
* @see https://github.com/php-fast-forward/
* @see https://github.com/php-fast-forward/dev-tools
* @see https://github.com/php-fast-forward/dev-tools/issues
* @see https://php-fast-forward.github.io/dev-tools/
* @see https://datatracker.ietf.org/doc/html/rfc2119
*/

namespace FastForward\DevTools\Resource;

/**
* Carries the result of comparing an overwrite source and target pair.
*/
final readonly class OverwriteDiffResult
{
/**
* @var string indicates that the source and target differ and a text diff is available
*/
public const string STATUS_CHANGED = 'changed';

/**
* @var string indicates that the source and target already match
*/
public const string STATUS_UNCHANGED = 'unchanged';

/**
* @var string indicates that a text diff should not be rendered for the compared files
*/
public const string STATUS_BINARY = 'binary';

/**
* @var string indicates that the compared files could not be read safely
*/
public const string STATUS_UNREADABLE = 'unreadable';

/**
* Creates a new overwrite diff result.
*
* @param string $status the comparison status for the source and target files
* @param string $summary the human-readable summary for console output
* @param string|null $diff the optional unified diff payload
*/
public function __construct(
private string $status,
private string $summary,
private ?string $diff = null,
) {
}

/**
* Returns the comparison status.
*
* @return string the comparison status value
*/
public function status(): string
{
return $this->status;
}

/**
* Returns the human-readable summary.
*
* @return string the summary for console output
*/
public function summary(): string
{
return $this->summary;
}

/**
* Returns the optional unified diff payload.
*
* @return string|null the diff payload, or null when no text diff is available
*/
public function diff(): ?string
{
return $this->diff;
}

/**
* Reports whether the compared files already match.
*
* @return bool true when the source and target contents are identical
*/
public function isUnchanged(): bool
{
return self::STATUS_UNCHANGED === $this->status;
}

/**
* Reports whether the compared files produced a text diff.
*
* @return bool true when a text diff is available
*/
public function isChanged(): bool
{
return self::STATUS_CHANGED === $this->status;
}
}
Loading
Loading