diff --git a/cli/Commands/Composer/GenerateUpdateList.php b/cli/Commands/Composer/GenerateUpdateList.php new file mode 100644 index 0000000000000000000000000000000000000000..bbce6a34bef954ac7dded16ef6e952f0b74d2051 --- /dev/null +++ b/cli/Commands/Composer/GenerateUpdateList.php @@ -0,0 +1,151 @@ +<?php +namespace Studip\Cli\Commands\Composer; + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Process\Exception\ProcessFailedException; +use Symfony\Component\Process\Process; + +final class GenerateUpdateList extends Command +{ + protected static $defaultName = 'composer:outdated'; + + protected function configure(): void + { + $this->setDescription('Generate markdown list of outdated packages'); + $this->setHelp('This command will create a markdown list of all outdated packages.'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + try { + $process = new Process(['composer', 'outdated', '-D', '--locked', '--format=json']); + $process->mustRun(); + + $json = $process->getOutput(); + if ($json) { + $output->writeln($json, OutputInterface::VERBOSITY_VERBOSE); + } + } catch (ProcessFailedException $e) { + $output->writeln("<error>Could not execute shell command</error>"); + $output->writeln($e->getmessage()); + return Command::FAILURE; + } + + $list = json_decode($json, true); + + $packages = [ + 'major' => [ + 'title' => 'Major updates', + 'items' => [], + ], + 'minor' => [ + 'title' => 'Minor updates', + 'items' => [], + ], + 'abandoned' => [ + 'title' => 'Abandoned packages', + 'items' => [], + ], + ]; + + foreach ($list['locked'] as $package) { + if ($package['abandoned']) { + $packages['abandoned']['items'][] = $package; + } elseif ($package['latest-status'] === 'semver-safe-update') { + $packages['minor']['items'][] = $package; + } else { + $packages['major']['items'][] = $package; + } + } + + + foreach ($packages as $p) { + $this->outputMarkdownListOfPackages($output, $p); + } + + return Command::SUCCESS; + } + + private function outputMarkdownListOfPackages( + OutputInterface $output, + array $packages + ): void + { + if (count($packages['items']) === 0) { + return; + } + + $output->writeln("# {$packages['title']}"); + $output->writeln(''); + + $headers = [ + 'issue' => 'Issue', + 'name' => 'Package', + 'version' => 'Installiert', + 'latest' => 'Verfügbar', + ]; + + $rows = []; + foreach ($packages['items'] as $package) { + $name = $package['name']; + if ($package['homepage']) { + $name = "[{$name}]({$package['homepage']})"; + } elseif ($package['source']) { + $name = "[{$name}]({$package['source']})"; + } + + $row = [ + 'issue' => '', + 'name' => $name, + 'version' => $package['version'], + 'latest' => $package['latest'], + ]; + + $rows[] = $row; + } + + $pad_sizes = $this->getPadSizes($headers, ...$rows); + + // Output headers + $this->outputTableRow($output, $headers, $pad_sizes); + + // Output dividers + $this->outputTableRow($output, array_fill_keys(array_keys($headers), ''), $pad_sizes, '-'); + + // Output all rows + foreach ($rows as $row) { + $this->outputTableRow($output, $row, $pad_sizes); + } + + $output->writeln(''); + } + + private function outputTableRow(OutputInterface $output, array $row, array $pad_sizes = [], string $pad = ' '): void + { + $items = []; + foreach ($row as $key => $value) { + $items[] = str_pad($value, $pad_sizes[$key] ?? 0, $pad); + } + + $output->writeln('| ' . implode(' | ', $items) . ' |'); + } + + private function getPadSizes(array ...$rows): array + { + $sizes = []; + + foreach ($rows as $row) { + foreach ($row as $key => $value) { + if (!isset($sizes[$key])) { + $sizes[$key] = mb_strlen($value); + } else { + $sizes[$key] = max($sizes[$key], mb_strlen($value)); + } + } + } + + return $sizes; + } +} diff --git a/cli/studip b/cli/studip index 36fe5f52dbdaecfa32db5fb66e950e9a7dbce147..6ba02ce51be78456aecd657b168aac0636c9335e 100755 --- a/cli/studip +++ b/cli/studip @@ -18,6 +18,7 @@ $commands = [ Commands\Checks\HelpTours::class, Commands\Checks\HelpTours::class, Commands\CleanupAdmissionRules::class, + Commands\Composer\GenerateUpdateList::class, Commands\Cronjobs\CronjobExecute::class, Commands\Cronjobs\CronjobExecute::class, Commands\Cronjobs\CronjobList::class,