diff --git a/CHANGELOG.md b/CHANGELOG.md index b1c4b011..158a2472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #474 Added compatibility to load ".tar.gz" archives in addition to ".tgz" - #469 Added ability to include an `IPMVersion` in `SystemRequirement` of module.xml - #530 Added a `CustomPhase` attribute to `` that doesn't require a corresponding %method in lifecycle class. +- #582 Added functionality to optionally see time of last update and server version of each package ### Changed - IPM is now namespace-specific rather than being installed in %SYS and being available instance-wide. diff --git a/src/cls/IPM/Main.cls b/src/cls/IPM/Main.cls index cff46f8b..6116d534 100644 --- a/src/cls/IPM/Main.cls +++ b/src/cls/IPM/Main.cls @@ -468,6 +468,8 @@ run C:\Temp\MyCommands.json, where contents of the file are as follows: + + @@ -633,6 +635,15 @@ generate /my/path -export 00000,PacketName2,IgnorePacket2^00000,PacketName3,Igno + + + Shows outdated modules and the latest version available in each repository. + + + Shows outdated modules. + + + } @@ -802,6 +813,8 @@ ClassMethod ShellInternal(pCommand As %String, Output pException As %Exception.A Do ..Namespace(.tCommandInfo) } ElseIf (tCommandInfo = "enable") { Do ..EnableIPM(.tCommandInfo) + } ElseIf (tCommandInfo = "list-outdated") { + Do ..ListOutdated(.tCommandInfo) } } Catch pException { If (pException.Code = $$$ERCTRLC) { @@ -949,7 +962,7 @@ ClassMethod GetListModules(pNamespace As %String = {$Namespace}, pSearch As %Str Quit } Set tArgs = 0 - Set tQuery = "SELECT Name,VersionString,Description,ExternalName,DeveloperMode,Root FROM %IPM_Storage.ModuleItem WHERE 1=1 " + Set tQuery = "SELECT Name,VersionString,Description,ExternalName,DeveloperMode,Root,LastUpdated FROM %IPM_Storage.ModuleItem WHERE 1=1 " Set pSearch = $ZStrip(pSearch, "<>WC") If pSearch'="" { If pSearch["," { @@ -982,7 +995,7 @@ ClassMethod GetListModules(pNamespace As %String = {$Namespace}, pSearch As %Str $$$ThrowOnError(tSC) Set name = tRes.Name Set list = list + 1 - Set list(list) = $ListBuild(name, tRes.VersionString, tRes.ExternalName, tRes.DeveloperMode, tRes.Root) + Set list(list) = $ListBuild(name, tRes.VersionString, tRes.ExternalName, tRes.DeveloperMode, tRes.Root, tRes.LastUpdated) If maxWidth<$Length(name) { Set maxWidth = $Length(name) } @@ -2179,7 +2192,7 @@ ClassMethod ListInstalled(ByRef pCommandInfo) [ Private ] } Else { Set showFields = "" If +$Get(pCommandInfo("data","Desc")) { - Set showFields = $ListBuild( + Set showFields = showFields_$ListBuild( "Description", "Author.CopyrightDate", "Author.License", @@ -2865,6 +2878,62 @@ ClassMethod EnableIPM(ByRef pCommandInfo) } } +ClassMethod CanUpdate(modName As %String, semver As %IPM.General.SemanticVersion) As %Boolean +{ + Do ..GetListModules($Namespace, "", .list) + For i=1:1:list { + Set $ListBuild(otherMod, version) = list(i) + If (otherMod = modName) { + Continue + } + Set otherMod = ##class(%IPM.Storage.Module).NameOpen(otherMod) + // There is no need to recurse here + For i = 1:1:otherMod.Dependencies.Count() { + If (otherMod.Dependencies.GetAt(i).Name '= modName) { + Continue + } + #dim expresion As %IPM.General.SemanticVersionExpression + Set expression = otherMod.Dependencies.GetAt(i).Version + If 'expression.IsSatisfiedBy(semver) { + Return 0 + } + } + } + Return 1 +} + +ClassMethod ListOutdated(ByRef pCommandInfo) +{ + Do ..GetListModules($Namespace, "", .list) + Do ..GetUpstreamPackageVersions(.serverVersions) + + For i=1:1:list { + Set $ListBuild(module, version) = list(i) + If $Data(serverVersions(module)) \ 10 = 0 { + Continue + } + Set semver = ##class(%IPM.General.SemanticVersion).FromString(version) + Set repo = "" + Set found = 0 + For { + Set repo = $Order(serverVersions(module, repo), 1, remoteVersion) + If repo = "" { + Quit + } + Set remoteSemver = ##class(%IPM.General.SemanticVersion).FromString(remoteVersion) + // TODO use better comparison method + If 'remoteSemver.Follows(semver) { + Continue + } + If 'found { + Set found = 1 + Write !, $$$FormattedLine($$$Green, module), " ", $$$FormattedLine($$$Blue, version) + } + Write " ", $$$FormattedLine($$$Red, $$$FormatText("%1:v%2", repo, remoteVersion)) + } + } +} + /// Runs package manager commands in a way that is friendly to the OS-level shell. /// Creates pOutputLogFile if it does not exist. /// If it does, and pAppendToLog is true, appends to it; otherwise, deletes the file before outputting to it. @@ -2951,6 +3020,11 @@ ClassMethod DisplayModules(ByRef pList, pNumbered As %Boolean = 0, pWithNamespac $ListBuild("Repository", $$$Blue) ) + Kill serverVersions + If $Data(pModifiers("showupstream")) { + Do ..GetUpstreamPackageVersions(.serverVersions) + } + Set width = $Get(pList("width")) + 1 Set tIndent = pIndent Set tIndent = $Select(pIndent > 0: pIndent, 1: width) @@ -2958,15 +3032,34 @@ ClassMethod DisplayModules(ByRef pList, pNumbered As %Boolean = 0, pWithNamespac Set info = pList(i) Set developerMode = 0 Set root = "" - Set $ListBuild(name, version, externalName, developerMode, root) = info + Set $ListBuild(name, version, externalName, developerMode, root, lastUpdated) = info Write:i>1 !,?pIndent Write $$$FormattedLinePadRight($$$Green, name, width), $$$FormattedLine($$$Blue, version) + If $Data(pModifiers("showupstream")) && ($Data(serverVersions(name)) / 10) { + Set r = "" + Set diffs = "" + For { + Set r = $Order(serverVersions(name, r), 1, sVer) + If r = "" { + Quit + } + If sVer '= version { + Set diffs = diffs_$ListBuild($$$FormatText("%1: %2", r, sVer)) + } + } + If diffs '= "" { + Write " ", $$$FormattedLine($$$Blue, "(" _ $ListToString(diffs, ", ") _ ")") + } + } If $Get(developerMode) { Write " ", $$$FormattedLine($$$Red, "(DeveloperMode)") } If $Data(pModifiers("showsource")) { Write " ", $$$FormattedLine($$$Magenta, root) } + If $Data(pModifiers("showtime")) { + Write " ", $$$FormattedLine($$$Yellow, lastUpdated) + } Set ptr = 0 While $ListNext(extraColumns, ptr, column) { @@ -2982,4 +3075,23 @@ ClassMethod DisplayModules(ByRef pList, pNumbered As %Boolean = 0, pWithNamespac } } +ClassMethod GetUpstreamPackageVersions(Output list) +{ + Set query = "SELECT %DLIST(Name) AS Repos FROM %IPM_Repo.Definition" + Set rs = ##class(%SQL.Statement).%ExecDirect(, query) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + If rs.%Next() { + Set repos = rs.%Get("Repos") + } + + Set ptr = 0 + While $ListNext(repos, ptr, r) { + Set query = "SELECT Name,Version FROM %IPM_Utils.Module_GetModuleList(?,?) " + Set rs = ##class(%SQL.Statement).%ExecDirect(, query, r, 0) + While rs.%Next() { + Set list(rs.%Get("Name"), r) = rs.%Get("Version") + } + } +} + } diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index 8511163f..ed5e2300 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -29,6 +29,8 @@ Property Packaging As %String [ Required ]; Property Dependencies As list Of %IPM.Storage.ModuleReference(STORAGEDEFAULT = "array"); +Property LastUpdated As %TimeStamp; + Relationship Resources As %IPM.Storage.ResourceReference(XMLIO = "IN", XMLITEMNAME = "Resource", XMLPROJECTION = "WRAPPED", XMLREFERENCE = "COMPLETE") [ Cardinality = children, Inverse = Module ]; /// Calculated property used for XML output of Resources relationship (in a reasonable order: @@ -1458,6 +1460,19 @@ Method %Evaluate(pAttrValue As %String, ByRef pParams) As %String [ Internal ] Return attrValue } +/// This callback method is invoked by the %Save method to +/// provide notification that the object is being saved. It is called before +/// any data is written to disk. +/// +///

insert will be set to 1 if this object is being saved for the first time. +/// +///

If this method returns an error then the call to %Save will fail. +Method %OnBeforeSave(insert As %Boolean) As %Status [ Private, ServerOnly = 1 ] +{ + Set ..LastUpdated = $ZDateTime($Horolog, 3) + Quit $$$OK +} + Storage Default { @@ -1552,6 +1567,9 @@ Storage Default AvailabilityClass + +LastUpdated + ^IPM.Storage.ModuleD ModuleDefaultData