Add --sort-by-age to show/outdated commands, and also release date for latest package in --latest mode (#11762)
parent
c8f1028ef9
commit
53a1f32061
|
@ -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.
|
||||
|
@ -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.
|
||||
|
|
|
@ -40,6 +40,7 @@ class OutdatedCommand extends BaseCommand
|
|||
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.'),
|
||||
|
@ -97,6 +98,9 @@ EOT
|
|||
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;
|
||||
|
|
|
@ -98,6 +98,7 @@ class ShowCommand extends BaseCommand
|
|||
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']),
|
||||
|
@ -450,7 +451,7 @@ EOT
|
|||
if (isset($packages[$type])) {
|
||||
ksort($packages[$type]);
|
||||
|
||||
$nameLength = $versionLength = $latestLength = 0;
|
||||
$nameLength = $versionLength = $latestLength = $releaseDateLength = 0;
|
||||
|
||||
if ($showLatest && $showVersion) {
|
||||
foreach ($packages[$type] as $package) {
|
||||
|
@ -469,9 +470,20 @@ EOT
|
|||
$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 = [];
|
||||
|
@ -505,6 +517,17 @@ EOT
|
|||
$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);
|
||||
|
@ -552,7 +575,9 @@ EOT
|
|||
'nameLength' => $nameLength,
|
||||
'versionLength' => $versionLength,
|
||||
'latestLength' => $latestLength,
|
||||
'releaseDateLength' => $releaseDateLength,
|
||||
'writeLatest' => $writeLatest,
|
||||
'writeReleaseDate' => $writeReleaseDate,
|
||||
];
|
||||
if ($input->getOption('strict') && $hasOutdatedPackages) {
|
||||
$exitCode = 1;
|
||||
|
@ -588,11 +613,14 @@ EOT
|
|||
$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;
|
||||
|
@ -620,14 +648,14 @@ EOT
|
|||
$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');
|
||||
}
|
||||
|
@ -635,7 +663,7 @@ EOT
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -651,11 +679,12 @@ EOT
|
|||
/**
|
||||
* @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 !== '') {
|
||||
|
@ -674,10 +703,13 @@ EOT
|
|||
$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;
|
||||
}
|
||||
|
@ -806,14 +838,20 @@ EOT
|
|||
*/
|
||||
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;
|
||||
}
|
||||
|
@ -822,7 +860,7 @@ EOT
|
|||
$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));
|
||||
|
@ -993,6 +1031,10 @@ EOT
|
|||
} else {
|
||||
$json['path'] = null;
|
||||
}
|
||||
|
||||
if ($package->getReleaseDate() !== null) {
|
||||
$json['released'] = $package->getReleaseDate()->format(DATE_ATOM);
|
||||
}
|
||||
}
|
||||
|
||||
if ($latestPackage instanceof CompletePackageInterface && $latestPackage->isAbandoned()) {
|
||||
|
@ -1447,4 +1489,30 @@ EOT
|
|||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ use Composer\Pcre\Preg;
|
|||
use Composer\Pcre\Regex;
|
||||
use Composer\Repository\PlatformRepository;
|
||||
use Composer\Test\TestCase;
|
||||
use DateTimeImmutable;
|
||||
|
||||
class ShowCommandTest extends TestCase
|
||||
{
|
||||
|
@ -55,13 +56,14 @@ class ShowCommandTest extends TestCase
|
|||
|
||||
$pkg = self::getPackage('vendor/package', '1.0.0');
|
||||
$pkg->setDescription('description of installed package');
|
||||
$major = self::getPackage('outdated/major', '1.0.0');
|
||||
$major->setReleaseDate(new DateTimeImmutable());
|
||||
$minor = self::getPackage('outdated/minor', '1.0.0');
|
||||
$minor->setReleaseDate(new DateTimeImmutable('-2 years'));
|
||||
$patch = self::getPackage('outdated/patch', '1.0.0');
|
||||
$patch->setReleaseDate(new DateTimeImmutable('-2 weeks'));
|
||||
|
||||
$this->createInstalledJson([
|
||||
$pkg,
|
||||
self::getPackage('outdated/major', '1.0.0'),
|
||||
self::getPackage('outdated/minor', '1.0.0'),
|
||||
self::getPackage('outdated/patch', '1.0.0'),
|
||||
]);
|
||||
$this->createInstalledJson([$pkg, $major, $minor, $patch]);
|
||||
|
||||
$appTester = $this->getApplicationTester();
|
||||
$appTester->run(array_merge(['command' => 'show'], $command));
|
||||
|
@ -112,6 +114,21 @@ outdated/minor 1.0.0 <highlight>! 1.1.1</highlight>
|
|||
outdated/patch 1.0.0 <highlight>! 1.0.1</highlight>',
|
||||
];
|
||||
|
||||
yield 'outdated deps sorting by age' => [
|
||||
['command' => 'outdated', '--sort-by-age' => true],
|
||||
'Legend:
|
||||
! patch or minor release available - update recommended
|
||||
~ major release available - update possible
|
||||
|
||||
Direct dependencies required in composer.json:
|
||||
Everything up to date
|
||||
|
||||
Transitive dependencies not required in composer.json:
|
||||
outdated/minor 1.0.0 <highlight>! 1.1.1</highlight> 2 years old
|
||||
outdated/patch 1.0.0 <highlight>! 1.0.1</highlight> 2 weeks old
|
||||
outdated/major 1.0.0 ~ 2.0.0 from today',
|
||||
];
|
||||
|
||||
yield 'outdated deps with --direct only show direct deps with updated' => [
|
||||
['command' => 'outdated', '--direct' => true],
|
||||
'Legend:
|
||||
|
@ -533,7 +550,7 @@ OUTPUT;
|
|||
|
||||
public function testSelf(): void
|
||||
{
|
||||
$this->initTempComposer(['name' => 'vendor/package']);
|
||||
$this->initTempComposer(['name' => 'vendor/package', 'time' => date('Y-m-d')]);
|
||||
|
||||
$appTester = $this->getApplicationTester();
|
||||
$appTester->run(['command' => 'show', '--self' => true]);
|
||||
|
@ -542,6 +559,7 @@ OUTPUT;
|
|||
'descrip.' => '',
|
||||
'keywords' => '',
|
||||
'versions' => '* 1.0.0+no-version-set',
|
||||
'released' => date('Y-m-d'). ', today',
|
||||
'type' => 'library',
|
||||
'homepage' => '',
|
||||
'source' => '[] ',
|
||||
|
|
Loading…
Reference in New Issue