Skip to content

Commit

Permalink
Add --sort-by-age to show/outdated commands, and also release date fo…
Browse files Browse the repository at this point in the history
…r latest package in --latest mode (composer#11762)
  • Loading branch information
Seldaek authored and theoboldalex committed Jan 10, 2024
1 parent 942b6ef commit 595375c
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 19 deletions.
2 changes: 2 additions & 0 deletions doc/03-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,7 @@ php composer.phar show monolog/monolog 1.0.2
* **--major-only (-M):** Use with --latest or --outdated. Only shows packages that have major SemVer-compatible updates.
* **--minor-only (-m):** Use with --latest or --outdated. Only shows packages that have minor SemVer-compatible updates.
* **--patch-only:** Use with --latest or --outdated. Only shows packages that have patch-level SemVer-compatible updates.
* **--sort-by-age (-A):** Displays the installed version's age, and sorts packages oldest first. Use with the --latest or --outdated option.
* **--direct (-D):** Restricts the list of packages to your direct dependencies.
* **--strict:** Return a non-zero exit code when there are outdated packages.
* **--format (-f):** Lets you pick between text (default) or json output format.
Expand Down Expand Up @@ -589,6 +590,7 @@ The color coding is as such:
* **--major-only (-M):** Only shows packages that have major SemVer-compatible updates.
* **--minor-only (-m):** Only shows packages that have minor SemVer-compatible updates.
* **--patch-only (-p):** Only shows packages that have patch-level SemVer-compatible updates.
* **--sort-by-age (-A):** Displays the installed version's age, and sorts packages oldest first.
* **--format (-f):** Lets you pick between text (default) or json output format.
* **--no-dev:** Do not show outdated dev dependencies.
* **--locked:** Shows updates for packages from the lock file, regardless of what is currently in vendor dir.
Expand Down
4 changes: 4 additions & 0 deletions src/Composer/Command/OutdatedCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ protected function configure(): void
new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates.'),
new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates.'),
new InputOption('patch-only', 'p', InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates.'),
new InputOption('sort-by-age', 'A', InputOption::VALUE_NONE, 'Displays the installed version\'s age, and sorts packages oldest first.'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
new InputOption('ignore', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore specified package(s). Use it if you don\'t want to be informed about new versions of some packages.', null, $this->suggestInstalledPackage(false)),
new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables search in require-dev packages.'),
Expand Down Expand Up @@ -97,6 +98,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if ($input->getOption('no-dev')) {
$args['--no-dev'] = true;
}
if ($input->getOption('sort-by-age')) {
$args['--sort-by-age'] = true;
}
$args['--ignore-platform-req'] = $input->getOption('ignore-platform-req');
if ($input->getOption('ignore-platform-reqs')) {
$args['--ignore-platform-reqs'] = true;
Expand Down
92 changes: 80 additions & 12 deletions src/Composer/Command/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ protected function configure()
new InputOption('major-only', 'M', InputOption::VALUE_NONE, 'Show only packages that have major SemVer-compatible updates. Use with the --latest or --outdated option.'),
new InputOption('minor-only', 'm', InputOption::VALUE_NONE, 'Show only packages that have minor SemVer-compatible updates. Use with the --latest or --outdated option.'),
new InputOption('patch-only', null, InputOption::VALUE_NONE, 'Show only packages that have patch SemVer-compatible updates. Use with the --latest or --outdated option.'),
new InputOption('sort-by-age', 'A', InputOption::VALUE_NONE, 'Displays the installed version\'s age, and sorts packages oldest first. Use with the --latest or --outdated option.'),
new InputOption('direct', 'D', InputOption::VALUE_NONE, 'Shows only packages that are directly required by the root package'),
new InputOption('strict', null, InputOption::VALUE_NONE, 'Return a non-zero exit code when there are outdated packages'),
new InputOption('format', 'f', InputOption::VALUE_REQUIRED, 'Format of the output: text or json', 'text', ['json', 'text']),
Expand Down Expand Up @@ -458,7 +459,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
if (isset($packages[$type])) {
ksort($packages[$type]);

$nameLength = $versionLength = $latestLength = 0;
$nameLength = $versionLength = $latestLength = $releaseDateLength = 0;

if ($showLatest && $showVersion) {
foreach ($packages[$type] as $package) {
Expand All @@ -477,9 +478,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$writeVersion = !$input->getOption('name-only') && !$input->getOption('path') && $showVersion;
$writeLatest = $writeVersion && $showLatest;
$writeDescription = !$input->getOption('name-only') && !$input->getOption('path');
$writeReleaseDate = $writeLatest && $input->getOption('sort-by-age');

$hasOutdatedPackages = false;

if ($input->getOption('sort-by-age')) {
usort($packages[$type], function ($a, $b) {
if (is_object($a) && is_object($b)) {
return $a->getReleaseDate() <=> $b->getReleaseDate();
}

return 0;
});
}

$viewData[$type] = [];
foreach ($packages[$type] as $package) {
$packageViewData = [];
Expand Down Expand Up @@ -513,6 +525,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$packageViewData['version'] = $package->getFullPrettyVersion();
$versionLength = max($versionLength, strlen($package->getFullPrettyVersion()));
}
if ($writeReleaseDate) {
if ($package->getReleaseDate() !== null) {
$packageViewData['release-age'] = str_replace(' ago', ' old', $this->getRelativeTime($package->getReleaseDate()));
if (!str_contains($packageViewData['release-age'], ' old')) {
$packageViewData['release-age'] = 'from '.$packageViewData['release-age'];
}
$releaseDateLength = max($releaseDateLength, strlen($packageViewData['release-age']));
} else {
$packageViewData['release-age'] = '';
}
}
if ($writeLatest && $latestPackage) {
$packageViewData['latest'] = $latestPackage->getFullPrettyVersion();
$packageViewData['latest-status'] = $this->getUpdateStatus($latestPackage, $package);
Expand Down Expand Up @@ -560,7 +583,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'nameLength' => $nameLength,
'versionLength' => $versionLength,
'latestLength' => $latestLength,
'releaseDateLength' => $releaseDateLength,
'writeLatest' => $writeLatest,
'writeReleaseDate' => $writeReleaseDate,
];
if ($input->getOption('strict') && $hasOutdatedPackages) {
$exitCode = 1;
Expand Down Expand Up @@ -596,11 +621,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$nameLength = $viewMetaData[$type]['nameLength'];
$versionLength = $viewMetaData[$type]['versionLength'];
$latestLength = $viewMetaData[$type]['latestLength'];
$releaseDateLength = $viewMetaData[$type]['releaseDateLength'];
$writeLatest = $viewMetaData[$type]['writeLatest'];
$writeReleaseDate = $viewMetaData[$type]['writeReleaseDate'];

$versionFits = $nameLength + $versionLength + 3 <= $width;
$latestFits = $nameLength + $versionLength + $latestLength + 3 <= $width;
$descriptionFits = $nameLength + $versionLength + $latestLength + 24 <= $width;
$releaseDateFits = $nameLength + $versionLength + $latestLength + $releaseDateLength + 3 <= $width;
$descriptionFits = $nameLength + $versionLength + $latestLength + $releaseDateLength + 24 <= $width;

if ($latestFits && !$io->isDecorated()) {
$latestLength += 2;
Expand Down Expand Up @@ -628,22 +656,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$io->writeError('');
$io->writeError('<info>Direct dependencies required in composer.json:</>');
if (\count($directDeps) > 0) {
$this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
$this->printPackages($io, $directDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength);
} else {
$io->writeError('Everything up to date');
}
$io->writeError('');
$io->writeError('<info>Transitive dependencies not required in composer.json:</>');
if (\count($transitiveDeps) > 0) {
$this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
$this->printPackages($io, $transitiveDeps, $indent, $writeVersion && $versionFits, $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength);
} else {
$io->writeError('Everything up to date');
}
} else {
if ($writeLatest && \count($packages) === 0) {
$io->writeError('All your direct dependencies are up to date');
} else {
$this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength);
$this->printPackages($io, $packages, $indent, $writeVersion && $versionFits, $writeLatest && $latestFits, $writeDescription && $descriptionFits, $width, $versionLength, $nameLength, $latestLength, $writeReleaseDate && $releaseDateFits, $releaseDateLength);
}
}

Expand All @@ -659,11 +687,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* @param array<array{name: string, direct-dependency?: bool, version?: string, latest?: string, latest-status?: string, description?: string|null, path?: string|null, source?: string|null, homepage?: string|null, warning?: string, abandoned?: bool|string}> $packages
*/
private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength): void
private function printPackages(IOInterface $io, array $packages, string $indent, bool $writeVersion, bool $writeLatest, bool $writeDescription, int $width, int $versionLength, int $nameLength, int $latestLength, bool $writeReleaseDate, int $releaseDateLength): void
{
$padName = $writeVersion || $writeLatest || $writeDescription;
$padVersion = $writeLatest || $writeDescription;
$padLatest = $writeDescription;
$padName = $writeVersion || $writeLatest || $writeReleaseDate || $writeDescription;
$padVersion = $writeLatest || $writeReleaseDate || $writeDescription;
$padLatest = $writeDescription || $writeReleaseDate;
$padReleaseDate = $writeDescription;
foreach ($packages as $package) {
$link = $package['source'] ?? $package['homepage'] ?? '';
if ($link !== '') {
Expand All @@ -682,10 +711,13 @@ private function printPackages(IOInterface $io, array $packages, string $indent,
$latestVersion = str_replace(['up-to-date', 'semver-safe-update', 'update-possible'], ['=', '!', '~'], $updateStatus) . ' ' . $latestVersion;
}
$io->write(' <' . $style . '>' . str_pad($latestVersion, ($padLatest ? $latestLength : 0), ' ') . '</' . $style . '>', false);
if ($writeReleaseDate && isset($package['release-age'])) {
$io->write(' '.str_pad($package['release-age'], ($padReleaseDate ? $releaseDateLength : 0), ' '), false);
}
}
if (isset($package['description']) && $writeDescription) {
$description = strtok($package['description'], "\r\n");
$remaining = $width - $nameLength - $versionLength - 4;
$remaining = $width - $nameLength - $versionLength - $releaseDateLength - 4;
if ($writeLatest) {
$remaining -= $latestLength;
}
Expand Down Expand Up @@ -814,14 +846,20 @@ protected function printPackageInfo(CompletePackageInterface $package, array $ve
*/
protected function printMeta(CompletePackageInterface $package, array $versions, InstalledRepository $installedRepo, ?PackageInterface $latestPackage = null): void
{
$isInstalledPackage = !PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package);

$io = $this->getIO();
$io->write('<info>name</info> : ' . $package->getPrettyName());
$io->write('<info>descrip.</info> : ' . $package->getDescription());
$io->write('<info>keywords</info> : ' . implode(', ', $package->getKeywords() ?: []));
$this->printVersions($package, $versions, $installedRepo);
if ($isInstalledPackage && $package->getReleaseDate() !== null) {
$io->write('<info>released</info> : ' . $package->getReleaseDate()->format('Y-m-d') . ', ' . $this->getRelativeTime($package->getReleaseDate()));
}
if ($latestPackage) {
$style = $this->getVersionStyle($latestPackage, $package);
$io->write('<info>latest</info> : <'.$style.'>' . $latestPackage->getPrettyVersion() . '</'.$style.'>');
$releasedTime = $latestPackage->getReleaseDate() === null ? '' : ' released ' . $latestPackage->getReleaseDate()->format('Y-m-d') . ', ' . $this->getRelativeTime($latestPackage->getReleaseDate());
$io->write('<info>latest</info> : <'.$style.'>' . $latestPackage->getPrettyVersion() . '</'.$style.'>' . $releasedTime);
} else {
$latestPackage = $package;
}
Expand All @@ -830,7 +868,7 @@ protected function printMeta(CompletePackageInterface $package, array $versions,
$io->write('<info>homepage</info> : ' . $package->getHomepage());
$io->write('<info>source</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getSourceType(), $package->getSourceUrl(), $package->getSourceReference()));
$io->write('<info>dist</info> : ' . sprintf('[%s] <comment>%s</comment> %s', $package->getDistType(), $package->getDistUrl(), $package->getDistReference()));
if (!PlatformRepository::isPlatformPackage($package->getName()) && $installedRepo->hasPackage($package)) {
if ($isInstalledPackage) {
$path = $this->requireComposer()->getInstallationManager()->getInstallPath($package);
if (is_string($path)) {
$io->write('<info>path</info> : ' . realpath($path));
Expand Down Expand Up @@ -1001,6 +1039,10 @@ protected function printPackageInfoAsJson(CompletePackageInterface $package, arr
} else {
$json['path'] = null;
}

if ($package->getReleaseDate() !== null) {
$json['released'] = $package->getReleaseDate()->format(DATE_ATOM);
}
}

if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) {
Expand Down Expand Up @@ -1455,4 +1497,30 @@ private function getRepositorySet(Composer $composer): RepositorySet

return $this->repositorySet;
}

private function getRelativeTime(\DateTimeInterface $releaseDate): string
{
if ($releaseDate->format('Ymd') === date('Ymd')) {
return 'today';
}

$diff = $releaseDate->diff(new \DateTimeImmutable());
if ($diff->days < 7) {
return 'this week';
}

if ($diff->days < 14) {
return 'last week';
}

if ($diff->m < 1 && $diff->days < 31) {
return floor($diff->days / 7) . ' weeks ago';
}

if ($diff->y < 1) {
return $diff->m . ' month' . ($diff->m > 1 ? 's' : '') . ' ago';
}

return $diff->y . ' year' . ($diff->y > 1 ? 's' : '') . ' ago';
}
}

0 comments on commit 595375c

Please sign in to comment.