config_preview_deploy-1.0.0-alpha3/src/PatchTool.php
src/PatchTool.php
<?php
declare(strict_types=1);
namespace Drupal\config_preview_deploy;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\Process\Process;
/**
* Service for applying unified diff patches using CLI patch tool.
*/
class PatchTool {
use StringTranslationTrait;
/**
* The file system service.
*/
protected FileSystemInterface $fileSystem;
/**
* The logger service.
*/
protected LoggerChannelInterface $logger;
/**
* Constructs a PatchTool object.
*/
public function __construct(
FileSystemInterface $fileSystem,
LoggerChannelFactoryInterface $loggerFactory,
) {
$this->fileSystem = $fileSystem;
$this->logger = $loggerFactory->get('config_preview_deploy');
}
/**
* Check if CLI patch tool is available.
*/
public function isAvailable(): bool {
static $available = NULL;
if ($available === NULL) {
$process = new Process(['which', 'patch']);
$process->run();
$available = $process->isSuccessful();
}
return $available;
}
/**
* Get patch tool version and information.
*/
public function getToolInfo(): array {
if (!$this->isAvailable()) {
return ['available' => FALSE];
}
$process = new Process(['patch', '--version']);
$process->run();
$output = $process->getOutput();
$version = 'unknown';
// Extract version from output (e.g., "GNU patch 2.7.6")
if (preg_match('/patch\s+(\d+\.\d+(?:\.\d+)?)/', $output, $matches)) {
$version = $matches[1];
}
return [
'available' => TRUE,
'version' => $version,
'output' => $output,
];
}
/**
* Apply unified diff to files in a directory.
*
* @param string $unifiedDiff
* The complete unified diff content.
* @param string $baseDirectory
* The base directory containing files to patch.
*
* @throws \Drupal\config_preview_deploy\Exception\PatchException
* If patch application fails.
*/
public function applyMultiFileDiff(string $unifiedDiff, string $baseDirectory): void {
if (!$this->isAvailable()) {
throw new PatchException($this->t('Patch tool not available')->render());
}
if (!is_dir($baseDirectory)) {
throw new PatchException($this->t('Base directory does not exist: @dir', ['@dir' => $baseDirectory])->render());
}
// Execute patch command with diff via stdin.
$process = new Process([
'patch',
'--directory=' . $baseDirectory,
// Remove one directory level from paths.
'--strip=1',
// Don't create .orig files.
'--no-backup-if-mismatch',
// Minimize output.
'--silent',
]);
$process->setInput($unifiedDiff);
$process->setTimeout(60);
$process->run();
if (!$process->isSuccessful()) {
$errorOutput = $process->getErrorOutput();
$standardOutput = $process->getOutput();
$exitCode = $process->getExitCode();
// Combine both outputs to get complete error information.
$fullErrorMessage = trim($errorOutput . "\n" . $standardOutput);
// If no output at all, provide a meaningful default.
if (empty($fullErrorMessage)) {
$fullErrorMessage = "Patch command failed with exit code {$exitCode}";
}
$this->logger->error('Patch application failed: @error (exit code: @code)', [
'@error' => $fullErrorMessage,
'@code' => $exitCode,
]);
throw new PatchException(
$this->t('Patch application failed: @error', ['@error' => $fullErrorMessage])->render(),
$exitCode
);
}
}
}
