diff --git a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php index a7913b4b9..ca4c7b811 100644 --- a/system/src/Grav/Common/Upgrade/SafeUpgradeService.php +++ b/system/src/Grav/Common/Upgrade/SafeUpgradeService.php @@ -171,7 +171,8 @@ class SafeUpgradeService Folder::create(dirname($packagePath)); $this->reportProgress('installing', 'Preparing staged package...', null); - $this->stageExtractedPackage($extractedPath, $packagePath); + $stagingMode = $this->stageExtractedPackage($extractedPath, $packagePath); + $this->reportProgress('installing', 'Preparing staged package...', null, ['mode' => $stagingMode]); $this->carryOverRootDotfiles($packagePath); @@ -186,7 +187,6 @@ class SafeUpgradeService $this->reportProgress('snapshot', 'Creating backup snapshot...', null); $this->createBackupSnapshot($entries, $backupPath); - $this->syncGitDirectory($this->rootPath, $backupPath); $manifest = $this->buildManifest($stageId, $targetVersion, $packagePath, $backupPath, $entries); $manifestPath = $stagePath . DIRECTORY_SEPARATOR . 'manifest.json'; @@ -199,12 +199,10 @@ class SafeUpgradeService $this->copyEntries($entries, $packagePath, $this->rootPath, 'installing', 'Deploying'); } catch (Throwable $e) { $this->copyEntries($entries, $backupPath, $this->rootPath, 'installing', 'Restoring'); - $this->syncGitDirectory($backupPath, $this->rootPath); throw new RuntimeException('Failed to promote staged Grav release.', 0, $e); } $this->reportProgress('finalizing', 'Finalizing upgrade...', null); - $this->syncGitDirectory($backupPath, $this->rootPath); $this->persistManifest($manifest); $this->pruneOldSnapshots(); Folder::delete($stagePath); @@ -234,20 +232,22 @@ class SafeUpgradeService return $entries; } - private function stageExtractedPackage(string $sourcePath, string $packagePath): void + private function stageExtractedPackage(string $sourcePath, string $packagePath): string { if (is_dir($packagePath)) { Folder::delete($packagePath); } if (@rename($sourcePath, $packagePath)) { - return; + return 'move'; } Folder::create($packagePath); $entries = $this->collectPackageEntries($sourcePath); $this->copyEntries($entries, $sourcePath, $packagePath, 'installing', 'Staging'); Folder::delete($sourcePath); + + return 'copy'; } private function createBackupSnapshot(array $entries, string $backupPath): void @@ -360,7 +360,6 @@ class SafeUpgradeService $this->reportProgress('rollback', 'Restoring snapshot...', null); $this->copyEntries($entries, $backupPath, $this->rootPath, 'rollback', 'Restoring'); - $this->syncGitDirectory($backupPath, $this->rootPath); $this->markRollback($manifest['id']); return $manifest; @@ -552,14 +551,8 @@ class SafeUpgradeService continue; } - // Skip caches to avoid stale data. - if (in_array($relative, ['cache', 'tmp'], true)) { - Folder::create($stage); - continue; - } - - Folder::create(dirname($stage)); - Folder::rcopy($live, $stage, true); + // Use empty placeholders to preserve directory structure without duplicating data. + Folder::create($stage); } } @@ -713,25 +706,6 @@ class SafeUpgradeService * @param string $destination * @return void */ - private function syncGitDirectory(string $source, string $destination): void - { - if (!$source || !$destination) { - return; - } - - $sourceGit = rtrim($source, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.git'; - if (!is_dir($sourceGit)) { - return; - } - - $destinationGit = rtrim($destination, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . '.git'; - if (is_dir($destinationGit)) { - Folder::delete($destinationGit); - } - - Folder::rcopy($sourceGit, $destinationGit, true); - } - /** * Persist manifest into Grav data directory. * diff --git a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php index f34c1870e..f6fb1a7dd 100644 --- a/system/src/Grav/Console/Gpm/SelfupgradeCommand.php +++ b/system/src/Grav/Console/Gpm/SelfupgradeCommand.php @@ -46,6 +46,14 @@ class SelfupgradeCommand extends GpmCommand private $upgrader; /** @var string|null */ private $lastProgressMessage = null; + /** @var float|null */ + private $operationTimerStart = null; + /** @var string|null */ + private $currentProgressStage = null; + /** @var float|null */ + private $currentStageStartedAt = null; + /** @var array */ + private $currentStageExtras = []; /** @var string */ protected $all_yes; @@ -235,6 +243,7 @@ class SelfupgradeCommand extends GpmCommand $this->file = $this->download($update); $io->write(' |- Installing upgrade... '); + $this->operationTimerStart = microtime(true); $installation = $this->upgrade(); $error = 0; @@ -443,6 +452,13 @@ class SelfupgradeCommand extends GpmCommand $this->lastProgressMessage = null; $this->upgradeGrav($this->file); + $this->finalizeStageTracking(); + + $elapsed = null; + if (null !== $this->operationTimerStart) { + $elapsed = microtime(true) - $this->operationTimerStart; + $this->operationTimerStart = null; + } $errorCode = Installer::lastErrorCode(); if ($errorCode) { @@ -454,6 +470,10 @@ class SelfupgradeCommand extends GpmCommand return false; } + if (null !== $elapsed) { + $io->writeln(sprintf(' |- Safe upgrade staging completed in %s', $this->formatDuration($elapsed))); + } + $io->write("\x0D"); // extra white spaces to clear out the buffer properly $io->writeln(' |- Installing upgrade... ok '); @@ -528,13 +548,19 @@ class SelfupgradeCommand extends GpmCommand private function handleServiceProgress(string $stage, string $message, ?int $percent = null, array $extra = []): void { + $this->trackStageProgress($stage, $message, $extra); + if ($this->lastProgressMessage === $message) { return; } $this->lastProgressMessage = $message; $io = $this->getIO(); - $io->writeln(sprintf(' |- %s', $message)); + $suffix = ''; + if (null !== $percent) { + $suffix = sprintf(' (%d%%)', $percent); + } + $io->writeln(sprintf(' |- %s%s', $message, $suffix)); } private function ensureExecutablePermissions(): void @@ -560,4 +586,69 @@ class SelfupgradeCommand extends GpmCommand } } } + + private function trackStageProgress(string $stage, string $message, array $extra = []): void + { + $now = microtime(true); + + if (null !== $this->currentProgressStage && $stage !== $this->currentProgressStage && null !== $this->currentStageStartedAt) { + $elapsed = $now - $this->currentStageStartedAt; + $this->emitStageSummary($this->currentProgressStage, $elapsed, $this->currentStageExtras); + $this->currentStageExtras = []; + } + + if ($stage !== $this->currentProgressStage) { + $this->currentProgressStage = $stage; + $this->currentStageStartedAt = $now; + $this->currentStageExtras = []; + } + + if (!isset($this->currentStageExtras['label'])) { + $this->currentStageExtras['label'] = $message; + } + + if ($extra) { + $this->currentStageExtras = array_merge($this->currentStageExtras, $extra); + } + } + + private function finalizeStageTracking(): void + { + if (null !== $this->currentProgressStage && null !== $this->currentStageStartedAt) { + $elapsed = microtime(true) - $this->currentStageStartedAt; + $this->emitStageSummary($this->currentProgressStage, $elapsed, $this->currentStageExtras); + } + + $this->currentProgressStage = null; + $this->currentStageStartedAt = null; + $this->currentStageExtras = []; + } + + private function emitStageSummary(string $stage, float $seconds, array $extra = []): void + { + $io = $this->getIO(); + $label = $extra['label'] ?? ucfirst($stage); + $modeText = ''; + if (isset($extra['mode'])) { + $modeText = sprintf(' [%s]', $extra['mode']); + } + + $io->writeln(sprintf(' |- %s completed in %s%s', $label, $this->formatDuration($seconds), $modeText)); + } + + private function formatDuration(float $seconds): string + { + if ($seconds < 1) { + return sprintf('%0.3fs', $seconds); + } + + $minutes = (int)floor($seconds / 60); + $remaining = $seconds - ($minutes * 60); + + if ($minutes === 0) { + return sprintf('%0.1fs', $remaining); + } + + return sprintf('%dm %0.1fs', $minutes, $remaining); + } }