diff --git a/src/chocolatey.resources/helpers/ChocolateyTabExpansion.ps1 b/src/chocolatey.resources/helpers/ChocolateyTabExpansion.ps1 index cbd9fd908f..e63070ae1f 100644 --- a/src/chocolatey.resources/helpers/ChocolateyTabExpansion.ps1 +++ b/src/chocolatey.resources/helpers/ChocolateyTabExpansion.ps1 @@ -49,7 +49,7 @@ $commandOptions = @{ info = "--cert='' --certpassword='' --disable-repository-optimizations --include-configured-sources --local-only --password='' --prerelease --source='' --user='' --version=''" install = "--allow-downgrade --allow-empty-checksums --allow-empty-checksums-secure --apply-args-to-dependencies --apply-package-parameters-to-dependencies --cert='' --certpassword='' --disable-repository-optimizations --download-checksum='' --download-checksum-x64='' --download-checksum-type='' --download-checksum-type-x64='' --exit-when-reboot-detected --force-dependencies --forcex86 --ignore-checksum --ignore-dependencies --ignore-detected-reboot --ignore-package-exit-codes --include-configured-sources --install-arguments='' --not-silent --override-arguments --package-parameters='' --password='' --pin --prerelease --require-checksums --skip-hooks --skip-scripts --source='' --stop-on-first-failure --use-package-exit-codes --user='' --version=''" license = "" - list = "--by-id-only --by-tag-only --detail --exact --id-only --id-starts-with --ignore-pinned --include-programs --page='' --page-size='' --prerelease --source='' --version=''" + list = "--by-id-only --by-tag-only --detail --exact --id-only --id-starts-with --ignore-pinned --include-programs --order-by-last-updated-date --page='' --page-size='' --prerelease --show-last-updated-date --source='' --version=''" new = "--automaticpackage --download-checksum='' --download-checksum-x64='' --download-checksum-type='' --maintainer='' --name='' --output-directory='' --template='' --use-built-in-template --version=''" outdated = "--cert='' --certpassword='' --disable-repository-optimizations --ignore-pinned --ignore-unfound --include-configured-sources --password='' --prerelease --source='' --user=''" pack = "--output-directory='' --version=''" @@ -242,9 +242,9 @@ function Get-ChocoOrderByOptions { manually when the enum changes. .OUTPUTS - A string in the format "Id|LastPublished|Popularity|Title|Unsorted" + A string in the format "Id|LastPublished|Popularity|Title|Unsorted|LastUpdated" #> - return @("Id", "LastPublished", "Popularity", "Title", "Unsorted") + return @("Id", "LastPublished", "Popularity", "Title", "Unsorted", "LastUpdated") } function ChocolateyTabExpansion($lastBlock) { diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs index e380aca4cf..d6134952e7 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs @@ -138,7 +138,13 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi option => configuration.Verbose = option != null) .Add("ignore-pinned", "Ignore Pinned - Ignore pinned packages. Defaults to false.", - option => configuration.ListCommand.IgnorePinned = option != null); + option => configuration.ListCommand.IgnorePinned = option != null) + .Add("show-last-updated-date", + "Show Date - Shows the last date the package was installed/updated.", + option => configuration.ListCommand.ShowLastUpdatedDate = option != null) + .Add("order-by-last-updated-date", + "Order by Last Updated Date - Orders packages by date the package was installed/updated.", + option => configuration.ListCommand.OrderBy = PackageOrder.LastUpdated); } public virtual int Count(ChocolateyConfiguration config) diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index c0bfe9f9c0..ad0ddf7040 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -626,6 +626,7 @@ public ListCommandConfiguration() public bool ByTagOnly { get; set; } public bool IdStartsWith { get; set; } public bool IgnorePinned { get; set; } + public bool ShowLastUpdatedDate { get; set; } public PackageOrder OrderBy { get; set; } [Obsolete("This property is deprecated and will be removed in version 3.0. Use the 'OrderBy' property instead.")] diff --git a/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs b/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs index 697675b5aa..d5d90ac3d0 100644 --- a/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs +++ b/src/chocolatey/infrastructure.app/domain/ChocolateyPackageInformation.cs @@ -38,5 +38,6 @@ public ChocolateyPackageInformation(IPackageMetadata package) public string ExtraInformation { get; set; } public string DeploymentLocation { get; set; } public string SourceInstalledFrom { get; set; } + public string LastUpdated { get; set; } } } diff --git a/src/chocolatey/infrastructure.app/domain/PackageOrder.cs b/src/chocolatey/infrastructure.app/domain/PackageOrder.cs index 0db14a85b9..2b93c3d30a 100644 --- a/src/chocolatey/infrastructure.app/domain/PackageOrder.cs +++ b/src/chocolatey/infrastructure.app/domain/PackageOrder.cs @@ -38,6 +38,11 @@ public enum PackageOrder /// LastPublished, + /// + /// Sort by last updated date, from newest to oldest. + /// + LastUpdated, + /// /// Do not sort; return packages in the order received from the source. /// diff --git a/src/chocolatey/infrastructure.app/nuget/NugetList.cs b/src/chocolatey/infrastructure.app/nuget/NugetList.cs index f91762c2e2..ee03a640df 100644 --- a/src/chocolatey/infrastructure.app/nuget/NugetList.cs +++ b/src/chocolatey/infrastructure.app/nuget/NugetList.cs @@ -22,6 +22,7 @@ using chocolatey.infrastructure.tolerance; using chocolatey.infrastructure.app.configuration; using chocolatey.infrastructure.filesystem; +using chocolatey.infrastructure.app.services; using NuGet.Common; using NuGet.Packaging; using NuGet.Packaging.Core; @@ -36,6 +37,8 @@ public static class NugetList public static bool ThresholdHit { get; private set; } public static bool LowerThresholdHit { get; private set; } + private const string LastUpdated = ".lastUpdated"; + public static IEnumerable GetPackages(ChocolateyConfiguration configuration, ILogger nugetLogger, IFileSystem filesystem) { return SearchPackagesAsync(configuration, nugetLogger, filesystem).GetAwaiter().GetResult(); @@ -320,7 +323,7 @@ private async static Task> SearchPackagesAsyn results = results.Where(p => (p.IsDownloadCacheAvailable && configuration.Information.IsLicensedVersion) || p.PackageTestResultStatus != "Failing").ToHashSet(); } - results = ApplyPackageSort(results, configuration.ListCommand.OrderBy).ToHashSet(); + results = ApplyPackageSort(results, configuration.ListCommand.OrderBy, filesystem).ToHashSet(); return results.AsQueryable(); } @@ -453,7 +456,7 @@ public static IPackageSearchMetadata FindPackage( return packagesList.OrderByDescending(p => p.Identity.Version).FirstOrDefault(); } - private static IOrderedEnumerable ApplyPackageSort(IEnumerable query, domain.PackageOrder orderBy) + private static IOrderedEnumerable ApplyPackageSort(IEnumerable query, domain.PackageOrder orderBy, IFileSystem filesystem) { switch (orderBy) { @@ -481,6 +484,31 @@ private static IOrderedEnumerable ApplyPackageSort(IEnum .ThenBy(q => q.Identity.Id) .ThenByDescending(q => q.Identity.Version); + case domain.PackageOrder.LastUpdated: + return query + .OrderByDescending(q => + { + string lastUpdatedContent = null; + var pkgStorePath = ChocolateyPackageInformationService.GetStorePath(filesystem, q.Identity.Id, q.Identity.Version); + var lastUpdated = filesystem.CombinePaths(pkgStorePath, LastUpdated); + if (filesystem.FileExists(lastUpdated)) + { + FaultTolerance.TryCatchWithLoggingException( + () => + { + lastUpdatedContent = filesystem.ReadFile(lastUpdated); + }, + "Unable to read last updated from file", + throwError: false, + logWarningInsteadOfError: true + ); + } + return lastUpdatedContent; + }) + .ThenBy(q => q.Identity.Id) + .ThenByDescending(q => q.Identity.Version); + + default: // Since we return an IOrderedEnumerable, some form of ordering must be applied, // even when the user has not explicitly requested a sort order. diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs index 3682325228..6be2845602 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageInformationService.cs @@ -46,6 +46,7 @@ public class ChocolateyPackageInformationService : IChocolateyPackageInformation private const string VersionOverrideFile = ".version"; private const string DeploymentLocationFile = ".deploymentLocation"; private const string SourceInstalledFromFile = ".sourceInstalledFrom"; + private const string LastUpdated = ".lastUpdated"; // We need to store the package identifiers we have warned about // to prevent duplicated outputs. @@ -208,6 +209,20 @@ has errored attempting to read it. This file will be renamed to logWarningInsteadOfError: true ); } + + var lastUpdated = _fileSystem.CombinePaths(pkgStorePath, LastUpdated); + if (_fileSystem.FileExists(lastUpdated)) + { + FaultTolerance.TryCatchWithLoggingException( + () => + { + packageInformation.LastUpdated = _fileSystem.ReadFile(lastUpdated); + }, + "Unable to read last updated from file", + throwError: false, + logWarningInsteadOfError: true + ); + } return packageInformation; } @@ -340,6 +355,23 @@ public void Save(ChocolateyPackageInformation packageInformation) { _fileSystem.DeleteFile(_fileSystem.CombinePaths(pkgStorePath, SourceInstalledFromFile)); } + + if (!string.IsNullOrWhiteSpace(packageInformation.LastUpdated)) + { + var lastUpdatedDate = _fileSystem.CombinePaths(pkgStorePath, LastUpdated); + if (_fileSystem.FileExists(lastUpdatedDate)) + { + _fileSystem.DeleteFile(lastUpdatedDate); + } + + _fileSystem.WriteFile(lastUpdatedDate, packageInformation.LastUpdated); + } + else + { + _fileSystem.DeleteFile(_fileSystem.CombinePaths(pkgStorePath, LastUpdated)); + } + + } public void Remove(IPackageMetadata package) @@ -353,7 +385,7 @@ public void Remove(IPackageMetadata package) _fileSystem.DeleteDirectoryChecked(pkgStorePath, recursive: true); } - private static string GetStorePath(IFileSystem fileSystem, string id, NuGetVersion version) + internal static string GetStorePath(IFileSystem fileSystem, string id, NuGetVersion version) { var preferredStorePath = fileSystem.CombinePaths(ApplicationParameters.ChocolateyPackageInfoStoreLocation, "{0}.{1}".FormatWith(id, version.ToNormalizedStringChecked())); diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index 14742a25a2..e10fdee351 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -566,6 +566,7 @@ public virtual void HandlePackageResult(PackageResult packageResult, ChocolateyC pkgInfo.DeploymentLocation = Environment.GetEnvironmentVariable(EnvironmentVariables.Package.ChocolateyPackageInstallLocation); pkgInfo.SourceInstalledFrom = packageResult.SourceInstalledFrom; + pkgInfo.LastUpdated = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); UpdatePackageInformation(pkgInfo); EnsureBadPackagesPathIsClean(packageResult); diff --git a/src/chocolatey/infrastructure.app/services/NugetService.cs b/src/chocolatey/infrastructure.app/services/NugetService.cs index 7540f58abc..080f2f0637 100644 --- a/src/chocolatey/infrastructure.app/services/NugetService.cs +++ b/src/chocolatey/infrastructure.app/services/NugetService.cs @@ -219,6 +219,7 @@ it is possible that incomplete package lists are returned from a command string packageInstallLocation = null; string deploymentlocation = null; string sourceInstalledFrom = null; + string lastUpdated = null; if (package.PackagePath != null && !string.IsNullOrWhiteSpace(package.PackagePath)) { @@ -240,6 +241,7 @@ it is possible that incomplete package lists are returned from a command packageInfo = _packageInfoService.Get(packageLocalMetadata); deploymentlocation = packageInfo.DeploymentLocation; sourceInstalledFrom = packageInfo.SourceInstalledFrom; + lastUpdated = packageInfo.LastUpdated; if (config.ListCommand.IncludeVersionOverrides) { @@ -281,11 +283,12 @@ it is possible that incomplete package lists are returned from a command { if (!(packageInfo != null && packageInfo.IsPinned && config.ListCommand.IgnorePinned)) { - this.Log().Info(logger, () => "{0}{1}".FormatWith(package.Identity.Id, config.ListCommand.IdOnly ? string.Empty : " {0}{1}{2}{3}".FormatWith( + this.Log().Info(logger, () => "{0}{1}".FormatWith(package.Identity.Id, config.ListCommand.IdOnly ? string.Empty : " {0}{1}{2}{3}{4}".FormatWith( packageLocalMetadata != null ? packageLocalMetadata.Version.ToFullStringChecked() : package.Identity.Version.ToFullStringChecked(), package.IsApproved ? " [Approved]" : string.Empty, package.IsDownloadCacheAvailable ? " Downloads cached for licensed users" : string.Empty, - package.PackageTestResultStatus == "Failing" && package.IsDownloadCacheAvailable ? " - Possibly broken for FOSS users (due to original download location changes by vendor)" : package.PackageTestResultStatus == "Failing" ? " - Possibly broken" : string.Empty + package.PackageTestResultStatus == "Failing" && package.IsDownloadCacheAvailable ? " - Possibly broken for FOSS users (due to original download location changes by vendor)" : package.PackageTestResultStatus == "Failing" ? " - Possibly broken" : string.Empty, + config.ListCommand.ShowLastUpdatedDate ? " {0}".FormatWith(packageInfo.LastUpdated) ?? " Last updated not available" : string.Empty )) ); @@ -299,7 +302,7 @@ Package url{6} Tags: {9} Software Site: {10} Software License: {11}{12}{13}{14}{15}{16} - Description: {17}{18}{19}{20}{21} + Description: {17}{18}{19}{20}{21}{22} ".FormatWith( package.Title.EscapeCurlyBraces(), package.Published.GetValueOrDefault().UtcDateTime.ToShortDateString(), @@ -336,6 +339,7 @@ Package url{6} !string.IsNullOrWhiteSpace(package.ReleaseNotes.ToStringSafe()) ? "{0} Release Notes: {1}".FormatWith(Environment.NewLine, package.ReleaseNotes.EscapeCurlyBraces().Replace("\n ", "\n").Replace("\n", "\n ")) : string.Empty, !string.IsNullOrWhiteSpace(deploymentlocation) ? "{0} Deployed to: '{1}'".FormatWith(Environment.NewLine, deploymentlocation) :string.Empty, !string.IsNullOrWhiteSpace(sourceInstalledFrom) ? "{0} Source package was installed from: '{1}'".FormatWith(Environment.NewLine, sourceInstalledFrom) : string.Empty, + !string.IsNullOrWhiteSpace(lastUpdated) ? "{0} Last updated: {1}".FormatWith(Environment.NewLine, lastUpdated) : string.Empty, packageArgumentsUnencrypted != null ? packageArgumentsUnencrypted : string.Empty )); } @@ -361,7 +365,7 @@ Package url{6} } else { - yield return new PackageResult(packageLocalMetadata, package, config.ListCommand.LocalOnly ? packageInstallLocation : null, config.Sources, null); + yield return new PackageResult(packageLocalMetadata, package, config.ListCommand.LocalOnly ? packageInstallLocation : null, config.Sources, null, lastUpdated); } } @@ -1001,7 +1005,7 @@ Version was specified as '{0}'. It is possible that version packageRemoteMetadata.PackageTestResultStatus == "Failing" && packageRemoteMetadata.IsDownloadCacheAvailable ? " - Likely broken for FOSS users (due to download location changes)" : packageRemoteMetadata.PackageTestResultStatus == "Failing" ? " - Possibly broken" : string.Empty )); - var packageResult = packageResultsToReturn.GetOrAdd(packageDependencyInfo.Id.ToLowerSafe(), new PackageResult(packageMetadata, packageRemoteMetadata, installedPath, null, packageDependencyInfo.Source.ToStringSafe())); + var packageResult = packageResultsToReturn.GetOrAdd(packageDependencyInfo.Id.ToLowerSafe(), new PackageResult(packageMetadata, packageRemoteMetadata, installedPath, null, packageDependencyInfo.Source.ToStringSafe(), null)); if (shouldAddForcedResultMessage) { packageResult.Messages.Add(new ResultMessage(ResultType.Note, "Backing up and removing old version")); @@ -1826,7 +1830,7 @@ public virtual ConcurrentDictionary Upgrade(ChocolateyCon packageRemoteMetadata.PackageTestResultStatus == "Failing" && packageRemoteMetadata.IsDownloadCacheAvailable ? " - Likely broken for FOSS users (due to download location changes)" : packageRemoteMetadata.PackageTestResultStatus == "Failing" ? " - Possibly broken" : string.Empty )); - var upgradePackageResult = packageResultsToReturn.GetOrAdd(packageDependencyInfo.Id.ToLowerSafe(), new PackageResult(packageMetadata, packageRemoteMetadata, installedPath, null, packageDependencyInfo.Source.ToStringSafe())); + var upgradePackageResult = packageResultsToReturn.GetOrAdd(packageDependencyInfo.Id.ToLowerSafe(), new PackageResult(packageMetadata, packageRemoteMetadata, installedPath, null, packageDependencyInfo.Source.ToStringSafe(), null)); upgradePackageResult.ResetMetadata(packageMetadata, packageRemoteMetadata); upgradePackageResult.InstallLocation = installedPath; @@ -2902,7 +2906,7 @@ protected virtual void BackupAndRunBeforeModify( { "chocolatey".Log().Debug("Running beforeModify step for '{0}'", packageResult.PackageMetadata.Id); - var packageResultCopy = new PackageResult(packageResult.PackageMetadata, packageResult.SearchMetadata, packageResult.InstallLocation, packageResult.Source, null); + var packageResultCopy = new PackageResult(packageResult.PackageMetadata, packageResult.SearchMetadata, packageResult.InstallLocation, packageResult.Source, null, null); beforeModifyAction(packageResultCopy, config); diff --git a/src/chocolatey/infrastructure/results/PackageResult.cs b/src/chocolatey/infrastructure/results/PackageResult.cs index a64d5f8d7a..76295d5c31 100644 --- a/src/chocolatey/infrastructure/results/PackageResult.cs +++ b/src/chocolatey/infrastructure/results/PackageResult.cs @@ -71,6 +71,11 @@ public bool Warning /// The package source used to install the package. /// public string SourceInstalledFrom { get; set; } + + /// + /// When the packaage was last updated. + /// + public string LastUpdated { get; set; } public int ExitCode { get; set; } public void ResetMetadata(IPackageMetadata metadata, IPackageSearchMetadata search) @@ -141,8 +146,8 @@ public PackageResult(IPackageSearchMetadata packageSearch, string installLocatio } [Obsolete("This overload is deprecated and will be removed in v3.")] - public PackageResult(IPackageMetadata packageMetadata, IPackageSearchMetadata packageSearch, string installLocation, string source = null) - : this(packageMetadata, packageSearch, installLocation, source, null) { } + public PackageResult(IPackageMetadata packageMetadata, IPackageSearchMetadata packageSearch, string installLocation, string source = null, string lastUpdated = null) + : this(packageMetadata, packageSearch, installLocation, source, null, lastUpdated) { } /// /// Initializes a new instance of the class. @@ -152,12 +157,14 @@ public PackageResult(IPackageMetadata packageMetadata, IPackageSearchMetadata pa /// Assigned to /// Sources available during package installation. Assigned to /// The package source used to install the package. Assigned to - public PackageResult(IPackageMetadata packageMetadata, IPackageSearchMetadata packageSearch, string installLocation, string source, string sourceInstalledFrom) - : this(packageMetadata.Id, packageMetadata.Version.ToNormalizedStringChecked(), installLocation) + /// The last updated date of the package. Assigned to + public PackageResult(IPackageMetadata packageMetadata, IPackageSearchMetadata packageSearch, string installLocation, string source, string sourceInstalledFrom, string lastUpdated) + : this(packageMetadata.Id, packageMetadata.Version.ToNormalizedStringChecked(), installLocation, lastUpdated) { SearchMetadata = packageSearch; PackageMetadata = packageMetadata; SourceInstalledFrom = sourceInstalledFrom; + LastUpdated = lastUpdated; var sources = new List(); if (!string.IsNullOrEmpty(source)) {