From 6dbead6d109d02cda1103b771aacb3d0d386c8ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 2 Feb 2026 18:18:36 +0100 Subject: [PATCH] Centralized git logic from InstallationTypeDetector and UpdateChecker in GitVersionInfoProvider service --- src/Command/UpdateCommand.php | 3 +- src/Command/VersionCommand.php | 12 +- src/Controller/HomepageController.php | 8 +- src/Controller/ToolsController.php | 8 +- src/Controller/UpdateManagerController.php | 12 +- src/Services/Misc/GitVersionInfo.php | 83 ----------- .../System/GitVersionInfoProvider.php | 135 ++++++++++++++++++ src/Services/System/InstallationType.php | 65 +++++++++ .../System/InstallationTypeDetector.php | 92 ++---------- src/Services/System/UpdateChecker.php | 30 +--- src/State/PartDBInfoProvider.php | 8 +- 11 files changed, 242 insertions(+), 214 deletions(-) delete mode 100644 src/Services/Misc/GitVersionInfo.php create mode 100644 src/Services/System/GitVersionInfoProvider.php create mode 100644 src/Services/System/InstallationType.php diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index 4f2cae86..64fa2bad 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -23,7 +23,6 @@ declare(strict_types=1); namespace App\Command; -use App\Services\System\InstallationType; use App\Services\System\UpdateChecker; use App\Services\System\UpdateExecutor; use Symfony\Component\Console\Attribute\AsCommand; @@ -134,7 +133,7 @@ HELP // Handle --refresh option if ($input->getOption('refresh')) { $io->text('Refreshing version information...'); - $this->updateChecker->refreshGitInfo(); + $this->updateChecker->refreshVersionInfo(); $io->success('Version cache cleared.'); } diff --git a/src/Command/VersionCommand.php b/src/Command/VersionCommand.php index d2ce75e1..d09def8f 100644 --- a/src/Command/VersionCommand.php +++ b/src/Command/VersionCommand.php @@ -22,9 +22,9 @@ declare(strict_types=1); */ namespace App\Command; -use Symfony\Component\Console\Attribute\AsCommand; -use App\Services\Misc\GitVersionInfo; +use App\Services\System\GitVersionInfoProvider; use Shivas\VersioningBundle\Service\VersionManagerInterface; +use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -33,7 +33,7 @@ use Symfony\Component\Console\Style\SymfonyStyle; #[AsCommand('partdb:version|app:version', 'Shows the currently installed version of Part-DB.')] class VersionCommand extends Command { - public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfo $gitVersionInfo) + public function __construct(protected VersionManagerInterface $versionManager, protected GitVersionInfoProvider $gitVersionInfo) { parent::__construct(); } @@ -48,9 +48,9 @@ class VersionCommand extends Command $message = 'Part-DB version: '. $this->versionManager->getVersion()->toString(); - if ($this->gitVersionInfo->getGitBranchName() !== null) { - $message .= ' Git branch: '. $this->gitVersionInfo->getGitBranchName(); - $message .= ', Git commit: '. $this->gitVersionInfo->getGitCommitHash(); + if ($this->gitVersionInfo->getBranchName() !== null) { + $message .= ' Git branch: '. $this->gitVersionInfo->getBranchName(); + $message .= ', Git commit: '. $this->gitVersionInfo->getCommitHash(); } $io->success($message); diff --git a/src/Controller/HomepageController.php b/src/Controller/HomepageController.php index 076e790b..6192c249 100644 --- a/src/Controller/HomepageController.php +++ b/src/Controller/HomepageController.php @@ -24,8 +24,8 @@ namespace App\Controller; use App\DataTables\LogDataTable; use App\Entity\Parts\Part; -use App\Services\Misc\GitVersionInfo; use App\Services\System\BannerHelper; +use App\Services\System\GitVersionInfoProvider; use App\Services\System\UpdateAvailableManager; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; @@ -43,7 +43,7 @@ class HomepageController extends AbstractController #[Route(path: '/', name: 'homepage')] - public function homepage(Request $request, GitVersionInfo $versionInfo, EntityManagerInterface $entityManager, + public function homepage(Request $request, GitVersionInfoProvider $versionInfo, EntityManagerInterface $entityManager, UpdateAvailableManager $updateAvailableManager): Response { $this->denyAccessUnlessGranted('HAS_ACCESS_PERMISSIONS'); @@ -77,8 +77,8 @@ class HomepageController extends AbstractController return $this->render('homepage.html.twig', [ 'banner' => $this->bannerHelper->getBanner(), - 'git_branch' => $versionInfo->getGitBranchName(), - 'git_commit' => $versionInfo->getGitCommitHash(), + 'git_branch' => $versionInfo->getBranchName(), + 'git_commit' => $versionInfo->getCommitHash(), 'show_first_steps' => $show_first_steps, 'datatable' => $table, 'new_version_available' => $updateAvailableManager->isUpdateAvailable(), diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index d78aff62..f1ed888c 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -27,7 +27,7 @@ use App\Services\Attachments\AttachmentURLGenerator; use App\Services\Attachments\BuiltinAttachmentsFinder; use App\Services\Doctrine\DBInfoHelper; use App\Services\Doctrine\NatsortDebugHelper; -use App\Services\Misc\GitVersionInfo; +use App\Services\System\GitVersionInfoProvider; use App\Services\System\UpdateAvailableManager; use App\Settings\AppSettings; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -47,7 +47,7 @@ class ToolsController extends AbstractController } #[Route(path: '/server_infos', name: 'tools_server_infos')] - public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper, + public function systemInfos(GitVersionInfoProvider $versionInfo, DBInfoHelper $DBInfoHelper, NatsortDebugHelper $natsortDebugHelper, AttachmentSubmitHandler $attachmentSubmitHandler, UpdateAvailableManager $updateAvailableManager, AppSettings $settings): Response { @@ -55,8 +55,8 @@ class ToolsController extends AbstractController return $this->render('tools/server_infos/server_infos.html.twig', [ //Part-DB section - 'git_branch' => $versionInfo->getGitBranchName(), - 'git_commit' => $versionInfo->getGitCommitHash(), + 'git_branch' => $versionInfo->getBranchName(), + 'git_commit' => $versionInfo->getCommitHash(), 'default_locale' => $settings->system->localization->locale, 'default_timezone' => $settings->system->localization->timezone, 'default_currency' => $settings->system->localization->baseCurrency, diff --git a/src/Controller/UpdateManagerController.php b/src/Controller/UpdateManagerController.php index b247cb38..08f7c77f 100644 --- a/src/Controller/UpdateManagerController.php +++ b/src/Controller/UpdateManagerController.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Controller; +use App\Services\System\BackupManager; use App\Services\System\UpdateChecker; use App\Services\System\UpdateExecutor; use Shivas\VersioningBundle\Service\VersionManagerInterface; @@ -47,6 +48,7 @@ class UpdateManagerController extends AbstractController private readonly UpdateChecker $updateChecker, private readonly UpdateExecutor $updateExecutor, private readonly VersionManagerInterface $versionManager, + private readonly BackupManager $backupManager, #[Autowire(env: 'bool:DISABLE_WEB_UPDATES')] private readonly bool $webUpdatesDisabled = false, #[Autowire(env: 'bool:DISABLE_BACKUP_RESTORE')] @@ -96,7 +98,7 @@ class UpdateManagerController extends AbstractController 'is_maintenance' => $this->updateExecutor->isMaintenanceMode(), 'maintenance_info' => $this->updateExecutor->getMaintenanceInfo(), 'update_logs' => $this->updateExecutor->getUpdateLogs(), - 'backups' => $this->updateExecutor->getBackups(), + 'backups' => $this->backupManager->getBackups(), 'web_updates_disabled' => $this->webUpdatesDisabled, 'backup_restore_disabled' => $this->backupRestoreDisabled, ]); @@ -131,7 +133,7 @@ class UpdateManagerController extends AbstractController return $this->json(['error' => 'Invalid CSRF token'], Response::HTTP_FORBIDDEN); } - $this->updateChecker->refreshGitInfo(); + $this->updateChecker->refreshVersionInfo(); return $this->json([ 'success' => true, @@ -173,7 +175,7 @@ class UpdateManagerController extends AbstractController #[Route('/log/{filename}', name: 'admin_update_manager_log', methods: ['GET'])] public function viewLog(string $filename): Response { - $this->denyAccessUnlessGranted('@system.show_updates'); + $this->denyAccessUnlessGranted('@system.manage_updates'); // Security: Only allow viewing files from the update logs directory $logs = $this->updateExecutor->getUpdateLogs(); @@ -303,7 +305,7 @@ class UpdateManagerController extends AbstractController { $this->denyAccessUnlessGranted('@system.manage_updates'); - $details = $this->updateExecutor->getBackupDetails($filename); + $details = $this->backupManager->getBackupDetails($filename); if (!$details) { return $this->json(['error' => 'Backup not found'], 404); @@ -344,7 +346,7 @@ class UpdateManagerController extends AbstractController } // Verify the backup exists - $backupDetails = $this->updateExecutor->getBackupDetails($filename); + $backupDetails = $this->backupManager->getBackupDetails($filename); if (!$backupDetails) { $this->addFlash('error', 'Backup file not found.'); return $this->redirectToRoute('admin_update_manager'); diff --git a/src/Services/Misc/GitVersionInfo.php b/src/Services/Misc/GitVersionInfo.php deleted file mode 100644 index 3c079f4f..00000000 --- a/src/Services/Misc/GitVersionInfo.php +++ /dev/null @@ -1,83 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Services\Misc; - -use Symfony\Component\HttpKernel\KernelInterface; - -class GitVersionInfo -{ - protected string $project_dir; - - public function __construct(KernelInterface $kernel) - { - $this->project_dir = $kernel->getProjectDir(); - } - - /** - * Get the Git branch name of the installed system. - * - * @return string|null The current git branch name. Null, if this is no Git installation - */ - public function getGitBranchName(): ?string - { - if (is_file($this->project_dir.'/.git/HEAD')) { - $git = file($this->project_dir.'/.git/HEAD'); - $head = explode('/', $git[0], 3); - - if (!isset($head[2])) { - return null; - } - - return trim($head[2]); - } - - return null; // this is not a Git installation - } - - /** - * Get hash of the last git commit (on remote "origin"!). - * - * If this method does not work, try to make a "git pull" first! - * - * @param int $length if this is smaller than 40, only the first $length characters will be returned - * - * @return string|null The hash of the last commit, null If this is no Git installation - */ - public function getGitCommitHash(int $length = 7): ?string - { - $filename = $this->project_dir.'/.git/refs/remotes/origin/'.$this->getGitBranchName(); - if (is_file($filename)) { - $head = file($filename); - - if (!isset($head[0])) { - return null; - } - - $hash = $head[0]; - - return substr($hash, 0, $length); - } - - return null; // this is not a Git installation - } -} diff --git a/src/Services/System/GitVersionInfoProvider.php b/src/Services/System/GitVersionInfoProvider.php new file mode 100644 index 00000000..6d067333 --- /dev/null +++ b/src/Services/System/GitVersionInfoProvider.php @@ -0,0 +1,135 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Process\Process; + +/** + * This service provides information about the current Git installation (if any). + */ +final readonly class GitVersionInfoProvider +{ + public function __construct(#[Autowire(param: 'kernel.project_dir')] private string $project_dir) + { + } + + /** + * Check if the project directory is a Git repository. + * @return bool + */ + public function isGitRepo(): bool + { + return is_dir($this->getGitDirectory()); + } + + /** + * Get the path to the Git directory of the installed system without a trailing slash. + * Even if this is no Git installation, the path is returned. + * @return string The path to the Git directory of the installed system + */ + public function getGitDirectory(): string + { + return $this->project_dir . '/.git'; + } + + /** + * Get the Git branch name of the installed system. + * + * @return string|null The current git branch name. Null, if this is no Git installation + */ + public function getBranchName(): ?string + { + if (is_file($this->getGitDirectory() . '/HEAD')) { + $git = file($this->getGitDirectory() . '/HEAD'); + $head = explode('/', $git[0], 3); + + if (!isset($head[2])) { + return null; + } + + return trim($head[2]); + } + + return null; // this is not a Git installation + } + + /** + * Get hash of the last git commit (on remote "origin"!). + * + * If this method does not work, try to make a "git pull" first! + * + * @param int $length if this is smaller than 40, only the first $length characters will be returned + * + * @return string|null The hash of the last commit, null If this is no Git installation + */ + public function getCommitHash(int $length = 8): ?string + { + $filename = $this->getGitDirectory() . '/refs/remotes/origin/'.$this->getBranchName(); + if (is_file($filename)) { + $head = file($filename); + + if (!isset($head[0])) { + return null; + } + + $hash = $head[0]; + + return substr($hash, 0, $length); + } + + return null; // this is not a Git installation + } + + /** + * Get the Git remote URL of the installed system. + */ + public function getRemoteURL(): ?string + { + // Get remote URL + $configFile = $this->getGitDirectory() . '/config'; + if (file_exists($configFile)) { + $config = file_get_contents($configFile); + if (preg_match('#url = (.+)#', $config, $matches)) { + return trim($matches[1]); + } + } + + return null; // this is not a Git installation + } + + /** + * Check if there are local changes in the Git repository. + * Attention: This runs a git command, which might be slow! + * @return bool|null True if there are local changes, false if not, null if this is not a Git installation + */ + public function hasLocalChanges(): ?bool + { + $process = new Process(['git', 'status', '--porcelain'], $this->project_dir); + $process->run(); + if (!$process->isSuccessful()) { + return null; // this is not a Git installation + } + return !empty(trim($process->getOutput())); + } +} diff --git a/src/Services/System/InstallationType.php b/src/Services/System/InstallationType.php new file mode 100644 index 00000000..74479bb9 --- /dev/null +++ b/src/Services/System/InstallationType.php @@ -0,0 +1,65 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\System; + +/** + * Detects the installation type of Part-DB to determine the appropriate update strategy. + */ +enum InstallationType: string +{ + case GIT = 'git'; + case DOCKER = 'docker'; + case ZIP_RELEASE = 'zip_release'; + case UNKNOWN = 'unknown'; + + public function getLabel(): string + { + return match ($this) { + self::GIT => 'Git Clone', + self::DOCKER => 'Docker', + self::ZIP_RELEASE => 'Release Archive (ZIP File)', + self::UNKNOWN => 'Unknown', + }; + } + + public function supportsAutoUpdate(): bool + { + return match ($this) { + self::GIT => true, + self::DOCKER => false, + // ZIP_RELEASE auto-update not yet implemented + self::ZIP_RELEASE => false, + self::UNKNOWN => false, + }; + } + + public function getUpdateInstructions(): string + { + return match ($this) { + self::GIT => 'Run: php bin/console partdb:update', + self::DOCKER => 'Pull the new Docker image and recreate the container: docker-compose pull && docker-compose up -d', + self::ZIP_RELEASE => 'Download the new release ZIP from GitHub, extract it over your installation, and run: php bin/console doctrine:migrations:migrate && php bin/console cache:clear', + self::UNKNOWN => 'Unable to determine installation type. Please update manually.', + }; + } +} diff --git a/src/Services/System/InstallationTypeDetector.php b/src/Services/System/InstallationTypeDetector.php index 4d04c55b..9f9fbdb8 100644 --- a/src/Services/System/InstallationTypeDetector.php +++ b/src/Services/System/InstallationTypeDetector.php @@ -26,51 +26,9 @@ namespace App\Services\System; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Process\Process; -/** - * Detects the installation type of Part-DB to determine the appropriate update strategy. - */ -enum InstallationType: string +readonly class InstallationTypeDetector { - case GIT = 'git'; - case DOCKER = 'docker'; - case ZIP_RELEASE = 'zip_release'; - case UNKNOWN = 'unknown'; - - public function getLabel(): string - { - return match($this) { - self::GIT => 'Git Clone', - self::DOCKER => 'Docker', - self::ZIP_RELEASE => 'Release Archive', - self::UNKNOWN => 'Unknown', - }; - } - - public function supportsAutoUpdate(): bool - { - return match($this) { - self::GIT => true, - self::DOCKER => false, - // ZIP_RELEASE auto-update not yet implemented - self::ZIP_RELEASE => false, - self::UNKNOWN => false, - }; - } - - public function getUpdateInstructions(): string - { - return match($this) { - self::GIT => 'Run: php bin/console partdb:update', - self::DOCKER => 'Pull the new Docker image and recreate the container: docker-compose pull && docker-compose up -d', - self::ZIP_RELEASE => 'Download the new release ZIP from GitHub, extract it over your installation, and run: php bin/console doctrine:migrations:migrate && php bin/console cache:clear', - self::UNKNOWN => 'Unable to determine installation type. Please update manually.', - }; - } -} - -class InstallationTypeDetector -{ - public function __construct(#[Autowire(param: 'kernel.project_dir')] private readonly string $project_dir) + public function __construct(#[Autowire(param: 'kernel.project_dir')] private string $project_dir, private GitVersionInfoProvider $gitVersionInfoProvider) { } @@ -129,7 +87,7 @@ class InstallationTypeDetector */ public function isGitInstall(): bool { - return is_dir($this->project_dir . '/.git'); + return $this->gitVersionInfoProvider->isGitRepo(); } /** @@ -169,51 +127,21 @@ class InstallationTypeDetector /** * Get Git-specific information. + * @return array{branch: string|null, commit: string|null, remote_url: string|null, has_local_changes: bool} */ private function getGitInfo(): array { - $info = [ - 'branch' => null, - 'commit' => null, - 'remote_url' => null, - 'has_local_changes' => false, + return [ + 'branch' => $this->gitVersionInfoProvider->getBranchName(), + 'commit' => $this->gitVersionInfoProvider->getCommitHash(8), + 'remote_url' => $this->gitVersionInfoProvider->getRemoteURL(), + 'has_local_changes' => $this->gitVersionInfoProvider->hasLocalChanges() ?? false, ]; - - // Get branch - $headFile = $this->project_dir . '/.git/HEAD'; - if (file_exists($headFile)) { - $head = file_get_contents($headFile); - if (preg_match('#ref: refs/heads/(.+)#', $head, $matches)) { - $info['branch'] = trim($matches[1]); - } - } - - // Get remote URL - $configFile = $this->project_dir . '/.git/config'; - if (file_exists($configFile)) { - $config = file_get_contents($configFile); - if (preg_match('#url = (.+)#', $config, $matches)) { - $info['remote_url'] = trim($matches[1]); - } - } - - // Get commit hash - $process = new Process(['git', 'rev-parse', '--short', 'HEAD'], $this->project_dir); - $process->run(); - if ($process->isSuccessful()) { - $info['commit'] = trim($process->getOutput()); - } - - // Check for local changes - $process = new Process(['git', 'status', '--porcelain'], $this->project_dir); - $process->run(); - $info['has_local_changes'] = !empty(trim($process->getOutput())); - - return $info; } /** * Get Docker-specific information. + * @return array{container_id: string|null, image: string|null} */ private function getDockerInfo(): array { diff --git a/src/Services/System/UpdateChecker.php b/src/Services/System/UpdateChecker.php index 49a132ee..b7a90296 100644 --- a/src/Services/System/UpdateChecker.php +++ b/src/Services/System/UpdateChecker.php @@ -48,6 +48,7 @@ class UpdateChecker private readonly CacheInterface $updateCache, private readonly VersionManagerInterface $versionManager, private readonly PrivacySettings $privacySettings, private readonly LoggerInterface $logger, private readonly InstallationTypeDetector $installationTypeDetector, + private readonly GitVersionInfoProvider $gitVersionInfoProvider, #[Autowire(param: 'kernel.debug')] private readonly bool $is_dev_mode, #[Autowire(param: 'kernel.project_dir')] private readonly string $project_dir) { @@ -84,34 +85,15 @@ class UpdateChecker 'is_git_install' => false, ]; - $gitDir = $this->project_dir . '/.git'; - - if (!is_dir($gitDir)) { + if (!$this->gitVersionInfoProvider->isGitRepo()) { return $info; } $info['is_git_install'] = true; - // Get branch from HEAD file - $headFile = $gitDir . '/HEAD'; - if (file_exists($headFile)) { - $head = file_get_contents($headFile); - if (preg_match('#ref: refs/heads/(.+)#', $head, $matches)) { - $info['branch'] = trim($matches[1]); - } - } - - // Get current commit - $process = new Process(['git', 'rev-parse', '--short', 'HEAD'], $this->project_dir); - $process->run(); - if ($process->isSuccessful()) { - $info['commit'] = trim($process->getOutput()); - } - - // Check for local changes - $process = new Process(['git', 'status', '--porcelain'], $this->project_dir); - $process->run(); - $info['has_local_changes'] = !empty(trim($process->getOutput())); + $info['branch'] = $this->gitVersionInfoProvider->getBranchName(); + $info['commit'] = $this->gitVersionInfoProvider->getCommitHash(8); + $info['has_local_changes'] = $this->gitVersionInfoProvider->hasLocalChanges(); // Get commits behind (fetch first) if ($info['branch']) { @@ -151,7 +133,7 @@ class UpdateChecker /** * Force refresh git information by invalidating cache. */ - public function refreshGitInfo(): void + public function refreshVersionInfo(): void { $gitInfo = $this->getGitInfo(); if ($gitInfo['branch']) { diff --git a/src/State/PartDBInfoProvider.php b/src/State/PartDBInfoProvider.php index b3496cad..b29ef227 100644 --- a/src/State/PartDBInfoProvider.php +++ b/src/State/PartDBInfoProvider.php @@ -7,8 +7,8 @@ namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use App\ApiResource\PartDBInfo; -use App\Services\Misc\GitVersionInfo; use App\Services\System\BannerHelper; +use App\Services\System\GitVersionInfoProvider; use App\Settings\SystemSettings\CustomizationSettings; use App\Settings\SystemSettings\LocalizationSettings; use Shivas\VersioningBundle\Service\VersionManagerInterface; @@ -17,7 +17,7 @@ class PartDBInfoProvider implements ProviderInterface { public function __construct(private readonly VersionManagerInterface $versionManager, - private readonly GitVersionInfo $gitVersionInfo, + private readonly GitVersionInfoProvider $gitVersionInfo, private readonly BannerHelper $bannerHelper, private readonly string $default_uri, private readonly LocalizationSettings $localizationSettings, @@ -31,8 +31,8 @@ class PartDBInfoProvider implements ProviderInterface { return new PartDBInfo( version: $this->versionManager->getVersion()->toString(), - git_branch: $this->gitVersionInfo->getGitBranchName(), - git_commit: $this->gitVersionInfo->getGitCommitHash(), + git_branch: $this->gitVersionInfo->getBranchName(), + git_commit: $this->gitVersionInfo->getCommitHash(), title: $this->customizationSettings->instanceName, banner: $this->bannerHelper->getBanner(), default_uri: $this->default_uri,