From c09cf80a18f6f5e3e3ea52ed8a3c3c1c9e274308 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 7 May 2024 14:49:04 -0400 Subject: [PATCH 01/79] Add configuration setting for production decomposition --- cls/SourceControl/Git/Settings.cls | 5 ++++- cls/SourceControl/Git/Utils.cls | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index cc48ee3d..270eb12d 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -23,6 +23,9 @@ Property percentClassReplace As %String [ InitialExpression = {##class(SourceCon /// Git project settings are read-only in the web user interface Property settingsUIReadOnly As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).SettingsUIReadOnly()} ]; +/// Interoperability productions are source-controlled under separate files for each configuration item +Property decomposeProductions As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).DecomposeProductions()} ]; + /// Attribution: Git username for user ${username} Property gitUserName As %String(MAXLEN = 255) [ InitialExpression = {##class(SourceControl.Git.Utils).GitUserName()} ]; @@ -111,6 +114,7 @@ Method %Save() As %Status // update value of basicUserMode to reflect the updated setting for basicMode set ..userBasicMode = ##class(SourceControl.Git.Utils).UserBasicMode() + set @storage@("settings","decomposeProductions") = ..decomposeProductions kill @##class(SourceControl.Git.Utils).MappingsNode() merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings @@ -210,4 +214,3 @@ Method OnAfterConfigure() As %Boolean } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 45baebdd..2eba6b32 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -72,6 +72,11 @@ ClassMethod SettingsUIReadOnly() As %Status [ CodeMode = expression ] $Get(@..#Storage@("settings","settingsUIReadOnly"), 0) } +ClassMethod DecomposeProductions() As %Boolean [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","decomposeProductions"), 0) +} + /// Returns the current (or previous) value of the flag. ClassMethod Locked(newFlagValue As %Boolean) As %Boolean { From 0f58789d103df16c300e35f5223884fa18171d10 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 14 May 2024 11:02:07 -0400 Subject: [PATCH 02/79] Export decomposed production after save --- cls/SourceControl/Git/Extension.cls | 5 ++++ cls/SourceControl/Git/Utils.cls | 38 ++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 6cfb2305..db019522 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -332,6 +332,11 @@ Method ExternalName(InternalName As %String) As %String quit ##class(SourceControl.Git.Utils).ExternalName(InternalName) } +ClassMethod FullExternalName(InternalName As %String) As %String +{ + quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) +} + Method IsReadOnly(InternalName As %String) As %Boolean { quit ##class(SourceControl.Git.Utils).Locked() diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 2eba6b32..53edddca 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1483,7 +1483,12 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As }elseif (type = "csp") && ..IsCspFolder(InternalName) { $$$QuitOnError(..ExportRoutinesAux(InternalName , "/", 0, force, .filenames)) }else { - if ..IsTempFileOutdated(InternalName) || force { + // for now, if this is a production class and production decomposition is enabled, don't worry about timestamps. + if ..ItemIsProductionToDecompose(InternalName) { + write !, "exporting decomposed production ", InternalName + set name = $piece(InternalName,".",1,*-1) + $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProduction(name, "FullExternalName")) + } elseif ..IsTempFileOutdated(InternalName) || force { #dim filename as %String = ..FullExternalName(InternalName, .MappingExists) if (MappingExists = 0){ write "Did not find a matching mapping for """_InternalName_""". Skipping export." @@ -1507,6 +1512,18 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As quit $$$OK } +ClassMethod ItemIsProductionToDecompose(InternalName) +{ + set settings = ##class(SourceControl.Git.Settings).%New() + set name = $piece(InternalName,".",1,*-1) + set ext = $zconvert($piece(InternalName,".",*),"U") + return settings.decomposeProductions + && ((ext = "PTD") + || ((ext = "CLS") + && ##class(%Dictionary.ClassDefinition).%ExistsId(name) + && $classmethod(name, "%Extends","Ens.Production"))) +} + ClassMethod ExportProject(project As %String, force As %Boolean = 0, ByRef filenames) As %Status { #dim rs as %ResultSet = ##class(%ResultSet).%New("%Studio.Project:ProjectItemsList") @@ -2062,7 +2079,23 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S // If no specific mapping was specified (p=""), then return the whole csp filename; otherwise return the name without the mapped piece set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") - + } elseif usertype && (docclass = "Ens.Util.ProjectTextDocument") { + // If using sharded Production Exports, use PTD naming conventions + if default { + set nam = $replace(nam,"||","/") + } + set $ListBuild(productionDir, nam) = $ListFromString(nam, "/") + // Abbreviate "ProductionSettings" to "ProdStgs", "Settings" to "Stgs". + Set prefix = $Case($Extract(nam), "P":"ProdStgs-", "S":"Stgs-") + Set nam = prefix_$Piece(nam,"-",2,*) + set $ListBuild(itemName, itemClassName) = $ListFromString(nam, "|") + if $Get(itemClassName) '= "" { + // Prevent errors from exceeding max file length by hashing item Class name + set nam = productionDir_"/"_itemName_$zhex($zcrc(itemClassName,6)) + } else { + set nam = productionDir_"/"_nam + } + return $translate(found_$Replace(nam,".","_")_".xml","\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) if default{ @@ -2549,4 +2582,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status } } - From 10b6a2707dee211acebfce2ccdc66555cc6d3c9a Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 17 May 2024 10:48:02 -0400 Subject: [PATCH 03/79] External name to internal name logic for PTD - most of this should be refactored into %Studio.SourceControl.Production later. --- cls/SourceControl/Git/File.cls | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index ba624fc0..38f16c10 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -35,8 +35,38 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } new %SourceControl //don't trigger source hooks with this test load to get the Name set sc=$system.OBJ.Load(ExternalName,"-d",,.outName,1) - if (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { + set itemIsPTD = 0 + if $data(outName) = 11 { + set key = $order(outName("")) + while (key '= "") { + if ($zconvert($piece(outName,".",*),"U") = "PTD") { + set itemIsPTD = 1 + quit + } + set key = $order(outName(key)) + } + } + if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { + // Special handling for Production Text Documents, used for Source Controlling individual Production settings + set file = $piece(ExternalName, "/", *) + set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(ExternalName) + set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") + set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) + set productionClass = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") + if $extract(file,1,9) = "ProdStgs-" { + set internalName = productionClass_"||ProductionSettings-"_productionClass_".PTD" + } else { + // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name + set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") + set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) + set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") + set itemName = settingsPTD.GetValueAt("/Item/@Name") + set internalName = productionClass_"||"_itemName_"|"_itemClass_".PTD" + } + } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName + } + if (internalName '= "") { set inst.InternalName = internalName $$$ThrowOnError(inst.%Save()) } From be0f76f5189f7cffbd3ee500ad0d6499740ca2e2 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:00:16 -0400 Subject: [PATCH 04/79] Added default mapping for decomposed production items --- cls/SourceControl/Git/Utils.cls | 1 + 1 file changed, 1 insertion(+) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 53edddca..4002d1a4 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2522,6 +2522,7 @@ ClassMethod SetDefaultMappings(mappingsNode As %String) set @mappingsNode@("CLS","UnitTest")="test/" set @mappingsNode@("INC","*")="inc/" set @mappingsNode@("MAC","*")="rtn/" + set @mappingsNode@("PTD","*")="ptd/" } ClassMethod PrintStreams(streams... As %Stream.FileCharacter) From 88b16174945e27fe3740189bde7cd6dd8e9d4a95 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:03:51 -0400 Subject: [PATCH 05/79] work in progress - only export modified production items --- cls/SourceControl/Git/Extension.cls | 24 +++++++++++++++++++++- cls/SourceControl/Git/Utils.cls | 32 +++++++++++++++-------------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index db019522..5edad791 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -271,6 +271,25 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName) set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName) if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) { + // If this is a production class and production decomposition is enabled, call recursively on all modified production items. + if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { + do ##class(%Studio.SourceControl.Production).GetModifiedItemsAfterSave(InternalName, .productionItems) + set key = $order(productionItems("")) + while (key '= "") { + if productionItems(key) = "D" { + set itemFilename = ..ExternalName(key) + if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) && ##class(%File).Exists(itemFilename) { + $$$ThrowOnError(##class(SourceControl.Git.Change).AddDeletedToUncommitted(itemFilename, InternalName)) + $$$ThrowOnError(##class(SourceControl.Git.Utils).DeleteExternalFile(InternalName)) + } + } elseif '..IsInSourceControl(key) { + $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(key)) + } else { + $$$ThrowOnError(..OnAfterSave(key)) + } + set key = $order(productionItems(key)) + } + } set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName)) $$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName)) @@ -349,6 +368,10 @@ Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Bool { set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" + if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { + do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) + //TODO: if any modified items in this production class are edited by a different user, prohibit editing + } set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) if filename="" quit $$$OK @@ -403,4 +426,3 @@ Method AddToSourceControl(InternalName As %String, Description As %String = "") } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 4002d1a4..535fe5ba 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -906,7 +906,8 @@ ClassMethod IsCspFolder(InternalName As %String) As %Boolean /// pkg -- package
/// prj -- project
/// csp -- csp-page or csp-folder. See IsCspFolder
-/// csp -- any static file from csp-folder +/// csp -- any static file from csp-folder
+/// ptd -- interoperability production configuration item ClassMethod Type(InternalName As %String) As %String { #dim extension as %String = $zconvert($piece(InternalName,".",$length(InternalName,".")),"L") @@ -1483,12 +1484,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As }elseif (type = "csp") && ..IsCspFolder(InternalName) { $$$QuitOnError(..ExportRoutinesAux(InternalName , "/", 0, force, .filenames)) }else { - // for now, if this is a production class and production decomposition is enabled, don't worry about timestamps. - if ..ItemIsProductionToDecompose(InternalName) { - write !, "exporting decomposed production ", InternalName - set name = $piece(InternalName,".",1,*-1) - $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProduction(name, "FullExternalName")) - } elseif ..IsTempFileOutdated(InternalName) || force { + if (type = "ptd") || ..IsTempFileOutdated(InternalName) || force { #dim filename as %String = ..FullExternalName(InternalName, .MappingExists) if (MappingExists = 0){ write "Did not find a matching mapping for """_InternalName_""". Skipping export." @@ -1502,7 +1498,13 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As } set filenames($I(filenames)) = filename write !, "exporting new version of ", InternalName, " to ", filename - $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) + if (type = "ptd") { + $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportPTD(InternalName,"FullExternalName")) + } elseif (..ItemIsProductionToDecompose(InternalName, .productionName)) { + $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProductionWithoutDefinition(productionName,"FullExternalName")) + } else { + $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) + } $$$QuitOnError(..UpdateRoutineTSH(InternalName, $h)) if '##class(SourceControl.Git.Change).IsUncommitted(filename) { $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "add", InternalName, $username, "", 1, "", "", 0)) @@ -1512,16 +1514,16 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As quit $$$OK } -ClassMethod ItemIsProductionToDecompose(InternalName) +ClassMethod ItemIsProductionToDecompose(InternalName, Output productionName) { set settings = ##class(SourceControl.Git.Settings).%New() set name = $piece(InternalName,".",1,*-1) - set ext = $zconvert($piece(InternalName,".",*),"U") - return settings.decomposeProductions - && ((ext = "PTD") - || ((ext = "CLS") - && ##class(%Dictionary.ClassDefinition).%ExistsId(name) - && $classmethod(name, "%Extends","Ens.Production"))) + set decomposeProduction = settings.decomposeProductions && (..Type(InternalName) = "cls") + && $$$comClassDefined(name) && $classmethod(name, "%Extends","Ens.Production") + if decomposeProduction { + set productionName = name + } + return decomposeProduction } ClassMethod ExportProject(project As %String, force As %Boolean = 0, ByRef filenames) As %Status From aef81122a94938fd1242568532c1b45262ae2734 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 14 May 2024 11:02:07 -0400 Subject: [PATCH 06/79] Export decomposed production after save --- cls/SourceControl/Git/Extension.cls | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 5edad791..317e77a8 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -356,6 +356,11 @@ ClassMethod FullExternalName(InternalName As %String) As %String quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) } +ClassMethod FullExternalName(InternalName As %String) As %String +{ + quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) +} + Method IsReadOnly(InternalName As %String) As %Boolean { quit ##class(SourceControl.Git.Utils).Locked() From 273568c5f92eb81a9ade79aa70ac6b808e1543f6 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:24:57 -0400 Subject: [PATCH 07/79] remove method that got duplicated by bad resolve --- cls/SourceControl/Git/Extension.cls | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 317e77a8..5edad791 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -356,11 +356,6 @@ ClassMethod FullExternalName(InternalName As %String) As %String quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) } -ClassMethod FullExternalName(InternalName As %String) As %String -{ - quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) -} - Method IsReadOnly(InternalName As %String) As %Boolean { quit ##class(SourceControl.Git.Utils).Locked() From cd8eea075716b36a73fea7559c8e292dd1a1ed43 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:34:48 -0400 Subject: [PATCH 08/79] fix for add to source control because PTD internal name doesn't exist in database --- cls/SourceControl/Git/Utils.cls | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index f96e22df..4b21361e 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -661,7 +661,9 @@ ClassMethod AddToSourceControl(InternalName As %String) As %Status } for i=1:1:$Get(filenames) { - set FileInternalName = ##class(SourceControl.Git.Utils).NormalizeExtension(##class(SourceControl.Git.Utils).NameToInternalName(filenames(i), 0,,1)) + set ignoreNonexistent = (type '= "ptd") + set FileInternalName = ##class(SourceControl.Git.Utils).NormalizeExtension( + ##class(SourceControl.Git.Utils).NameToInternalName(filenames(i), 0,ignoreNonexistent,1)) if (FileInternalName = "") { continue } From 6885262a94d62350462b3559affa3640699ed8bc Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:42:45 -0400 Subject: [PATCH 09/79] more fixes to name logic for PTDs --- cls/SourceControl/Git/File.cls | 3 +-- cls/SourceControl/Git/Utils.cls | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 38f16c10..13a39d84 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -61,7 +61,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") set itemName = settingsPTD.GetValueAt("/Item/@Name") - set internalName = productionClass_"||"_itemName_"|"_itemClass_".PTD" + set internalName = productionClass_"||Settings-"_itemName_"|"_itemClass_".PTD" } } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName @@ -99,4 +99,3 @@ Storage Default } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 4b21361e..534b08bd 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1959,7 +1959,7 @@ ClassMethod GitStatus(ByRef files, IncludeAllFiles = 0) while $listnext(list, pointer, item) { set operation = $zstrip($extract(item, 1, 2), " Date: Thu, 18 Jul 2024 15:54:17 -0400 Subject: [PATCH 10/79] GetStatus logic to prohibit editing item checked out by other user --- cls/SourceControl/Git/Extension.cls | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 5edad791..f70b0d9f 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -370,7 +370,13 @@ Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Bool set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) - //TODO: if any modified items in this production class are edited by a different user, prohibit editing + set key = $order(productionItems("")) + while (key '= "") { + // if any modified items in this production class are checked out by a different user, fail the check. + set st = ..GetStatus(key, .IsInSourceControl, .Editable, .IsCheckedOut, .UserCheckedOut) + if 'Editable return st + set key = $order(productionItems(key)) + } } set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) From d5aaf6c3ec51201b36d5caa6ff4e406481c6071c Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:26:29 -0400 Subject: [PATCH 11/79] useful alert shown to user; remove spurious alert on loading config page if prod settings are edited by another user --- cls/SourceControl/Git/Extension.cls | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index f70b0d9f..69d9aca9 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -361,23 +361,35 @@ Method IsReadOnly(InternalName As %String) As %Boolean quit ##class(SourceControl.Git.Utils).Locked() } -/// Check the status of the given item -/// User should be able to edit the item if it is not locked by another user -/// Check if another user has committed any changes to the item and return the status -Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Boolean, ByRef Editable As %Boolean, ByRef IsCheckedOut As %Boolean, ByRef UserCheckedOut As %String) As %Status +/// Called before the item is saved to the database it is passed +/// a reference to the current temporary storage of this item so that it +/// can be modified before the save completes. If you quit with an error +/// value then it will abort the save. +Method OnBeforeSave(InternalName As %String, Location As %String = "", Object As %RegisteredObject = {$$$NULLOREF}) As %Status { - set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) - set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" + set st = $$$OK if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) set key = $order(productionItems("")) while (key '= "") { // if any modified items in this production class are checked out by a different user, fail the check. set st = ..GetStatus(key, .IsInSourceControl, .Editable, .IsCheckedOut, .UserCheckedOut) - if 'Editable return st + quit:$$$ISERR(st) + if 'Editable set st = $$$ERROR($$$GeneralError,"Item is checked out by another user: "_UserCheckedOut) + quit:$$$ISERR(st) set key = $order(productionItems(key)) } } + return st +} + +/// Check the status of the given item +/// User should be able to edit the item if it is not locked by another user +/// Check if another user has committed any changes to the item and return the status +Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Boolean, ByRef Editable As %Boolean, ByRef IsCheckedOut As %Boolean, ByRef UserCheckedOut As %String) As %Status +{ + set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) + set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) if filename="" quit $$$OK From 75d4ca08dd9c55345dcbf004aa90dfc20967b30c Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:04:05 -0400 Subject: [PATCH 12/79] import PTD uses deployment manager to apply setting --- cls/SourceControl/Git/Utils.cls | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 534b08bd..172364fd 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1261,8 +1261,14 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A #dim fileTSH = ##class(%File).GetFileDateModified(filename) #dim sc as %Status = $$$OK - if ..IsRoutineOutdated(InternalName) || force { - if ..UserTypeCached(InternalName,.docclass,.doctype) { + set type = ..Type(InternalName) + if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ + if (type = "ptd") && $$$comClassDefined("Ens.Deployment.Deploy") { + set targetProduction = $piece(InternalName,"||",1) + set rollbackFile = ##class(%File).TempFilename() + set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) + do ##class(%File).Delete(rollbackFile) + } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { set routineMgr = ##class(%RoutineMgr).%OpenId(InternalName) do routineMgr.Code.Rewind() set source = ##class(%Stream.FileCharacter).%OpenId(filename,,.sc) @@ -1279,7 +1285,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A } if sc { set sc = ..UpdateRoutineTSH(InternalName, fileTSH) - if ..Type(InternalName) = "prj" { + if type = "prj" { set sc = $$$ADDSC(sc, ..FixProjectCspReferences(InternalName)) } write !, InternalName," has been imported from ", filename From 5958615c6e4f8b7e5ca32c6fafc4def444dbdd12 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:51:02 -0400 Subject: [PATCH 13/79] Refactored naming logic into production class --- cls/SourceControl/Git/File.cls | 17 +---------------- cls/SourceControl/Git/Utils.cls | 20 +++----------------- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 13a39d84..292a8c33 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -47,22 +47,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } } if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - // Special handling for Production Text Documents, used for Source Controlling individual Production settings - set file = $piece(ExternalName, "/", *) - set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(ExternalName) - set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") - set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) - set productionClass = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") - if $extract(file,1,9) = "ProdStgs-" { - set internalName = productionClass_"||ProductionSettings-"_productionClass_".PTD" - } else { - // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name - set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") - set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) - set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") - set itemName = settingsPTD.GetValueAt("/Item/@Name") - set internalName = productionClass_"||Settings-"_itemName_"|"_itemClass_".PTD" - } + do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,,.internalName) } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName } diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 172364fd..ea5dec89 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2091,23 +2091,9 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S // If no specific mapping was specified (p=""), then return the whole csp filename; otherwise return the name without the mapped piece set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") - } elseif usertype && (docclass = "Ens.Util.ProjectTextDocument") { - // If using sharded Production Exports, use PTD naming conventions - if default { - set nam = $replace(nam,"||","/") - } - set $ListBuild(productionDir, nam) = $ListFromString(nam, "/") - // Abbreviate "ProductionSettings" to "ProdStgs", "Settings" to "Stgs". - Set prefix = $Case($Extract(nam), "P":"ProdStgs-", "S":"Stgs-", :"") - Set nam = prefix_$Piece(nam,"-",2,*) - set $ListBuild(itemName, itemClassName) = $ListFromString(nam, "|") - if $Get(itemClassName) '= "" { - // Prevent errors from exceeding max file length by hashing item Class name - set nam = productionDir_"/"_itemName_$zhex($zcrc(itemClassName,6)) - } else { - set nam = productionDir_"/"_nam - } - return $translate(found_$Replace(nam,".","_")_".xml","\","/") + } elseif (..Type(InternalName) = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { + do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,,.filename) + return $translate(found_filename, "\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) if default{ From 97364decf31da21615dec6be081daeb5daab5f54 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:19:35 -0400 Subject: [PATCH 14/79] remove unused argument on parseexternal/internalname --- cls/SourceControl/Git/File.cls | 2 +- cls/SourceControl/Git/Utils.cls | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 292a8c33..7a3ff455 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -47,7 +47,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } } if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,,.internalName) + do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,.internalName) } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName } diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 1b8125bb..70abd7bc 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2118,7 +2118,7 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") } elseif (..Type(InternalName) = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { - do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,,.filename) + do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,.filename) return $translate(found_filename, "\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) From d54b0e205b1aaabfb806b0a102429e48ba7231ed Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:58:08 -0400 Subject: [PATCH 15/79] import all now imports user types in subdirectories recursively --- cls/SourceControl/Git/Utils.cls | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 70abd7bc..dcfa0fed 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1316,6 +1316,24 @@ ClassMethod ImportCSPFile(InternalName As %String) As %Status Quit sc } +ClassMethod ListItemsRecursively(type, fileSpec, directory, ByRef itemList) [ Private ] +{ + set files = ##class(%Library.File).FileSetFunc(directory,fileSpec,,1) + throw:files.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(files.%SQLCODE,files.%Message) + while files.%Next() { + if (files.Type="D") { + do ..ListItemsRecursively(type, fileSpec, files.Name, .itemList) + } else { + set internalName = files.ItemName + if ($zconvert(type,"l") = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { + do ##class(%Studio.SourceControl.Production).ParseExternalName($translate(files.Name,"\","/"), .internalName) + } + set itemList(internalName) = "" + } + } +} + +/// Returns an array of internal names of all items in the local source control repository. ClassMethod ListItemsInFiles(ByRef itemList, ByRef err) As %Status { #define DoNotLoad 1 @@ -1332,13 +1350,11 @@ ClassMethod ListItemsInFiles(ByRef itemList, ByRef err) As %Status set mappedFilePath = ##class(%File).NormalizeFilename(mappedRelativePath, ..TempFolder()) if (##class(%File).DirectoryExists(mappedFilePath)){ - if ..UserTypeCached("foo."_mappingFileType) { - set fileSpec = "*."_$zcvt(mappingFileType,"L")_";*."_$zconvert(mappingFileType,"U") - set files = ##class(%Library.File).FileSetFunc(mappedFilePath,fileSpec) - while files.%Next() { - // Assumes flat file structure - set itemList(files.ItemName) = "" - } + if ..UserTypeCached("foo."_mappingFileType, .userTypeClass) { + set fileSpec = $select( + userTypeClass="Ens.Util.ProjectTextDocument": "*.xml;*.XML", + 1: "*."_$zcvt(mappingFileType,"L")_";*."_$zconvert(mappingFileType,"U")) + do ..ListItemsRecursively(mappingFileType, fileSpec, mappedFilePath, .itemList) } else { set res = $system.OBJ.ImportDir(mappedFilePath,,"-d",.err,1, .tempItemList, $$$DoNotLoad) merge itemList = tempItemList From 3733eb56d8895349a5fd026100af3149546dbb09 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:14:32 -0400 Subject: [PATCH 16/79] import production class without definition --- cls/SourceControl/Git/Utils.cls | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index dcfa0fed..44a807c7 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1261,6 +1261,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A #dim fileTSH = ##class(%File).GetFileDateModified(filename) #dim sc as %Status = $$$OK + set settings = ##class(SourceControl.Git.Settings).%New() set type = ..Type(InternalName) if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && $$$comClassDefined("Ens.Deployment.Deploy") { @@ -1268,6 +1269,11 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set rollbackFile = ##class(%File).TempFilename() set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) do ##class(%File).Delete(rollbackFile) + } elseif (type = "cls") && settings.decomposeProductions + && $$$comClassDefined("%Studio.SourceControl.Production") + && ##class(%Studio.SourceControl.Production).IsProductionClass( + ..NameWithoutExtension(InternalName), "FullExternalName") { + set sc = ##class(%Studio.SourceControl.Production).ImportProductionWithoutDefinition(filename) } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { set routineMgr = ##class(%RoutineMgr).%OpenId(InternalName) do routineMgr.Code.Rewind() From 67b1995ecd59e14115f494e141f960fe326924f6 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:26:34 -0400 Subject: [PATCH 17/79] refactor: better way of determining if namespace has Ensemble --- cls/SourceControl/Git/Utils.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 44a807c7..272c0ebf 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1264,7 +1264,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set settings = ##class(SourceControl.Git.Settings).%New() set type = ..Type(InternalName) if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ - if (type = "ptd") && $$$comClassDefined("Ens.Deployment.Deploy") { + if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { set targetProduction = $piece(InternalName,"||",1) set rollbackFile = ##class(%File).TempFilename() set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) From 47b25c747e3b52f7ed22f247d5681cf6c3ff3d33 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:49:04 -0400 Subject: [PATCH 18/79] fix deleting production items deletes file from source control --- cls/SourceControl/Git/Extension.cls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 854079f9..25b665b0 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -282,10 +282,10 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU set key = $order(productionItems("")) while (key '= "") { if productionItems(key) = "D" { - set itemFilename = ..ExternalName(key) - if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) && ##class(%File).Exists(itemFilename) { - $$$ThrowOnError(##class(SourceControl.Git.Change).AddDeletedToUncommitted(itemFilename, InternalName)) - $$$ThrowOnError(##class(SourceControl.Git.Utils).DeleteExternalFile(InternalName)) + set itemFilename = ..FullExternalName(key) + if ##class(SourceControl.Git.Utils).IsInSourceControl(key) && ##class(%File).Exists(itemFilename) { + $$$ThrowOnError(##class(SourceControl.Git.Change).AddDeletedToUncommitted(itemFilename, key)) + $$$ThrowOnError(##class(SourceControl.Git.Utils).DeleteExternalFile(key)) } } elseif '..IsInSourceControl(key) { $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(key)) From afb7e34c8e96daf31fbb856c01629868a127f634 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:11:32 -0400 Subject: [PATCH 19/79] fix issue where importing PTD items reexports them as changed --- cls/SourceControl/Git/Extension.cls | 1 + cls/SourceControl/Git/Utils.cls | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 25b665b0..f8299b9c 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -272,6 +272,7 @@ Method OnBeforeTimestamp(InternalName As %String) Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NULLOREF}) As %Status { set sc = $$$OK + quit:$get(%gscSkipSaveHooks) sc try { set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName) set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 272c0ebf..f285bb96 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1265,6 +1265,10 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set type = ..Type(InternalName) if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { + // Deployment manager should not reexport because studio project file includes timestamp + // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl + new %gscSkipSaveHooks + set %gscSkipSaveHooks = 1 set targetProduction = $piece(InternalName,"||",1) set rollbackFile = ##class(%File).TempFilename() set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) From 8e768445a98b1c308dc425b0a7f7dc6dde21a9c1 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:34:56 -0400 Subject: [PATCH 20/79] Production classes now excluded from export/import --- cls/SourceControl/Git/Utils.cls | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index f285bb96..1293bd76 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1263,6 +1263,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set settings = ##class(SourceControl.Git.Settings).%New() set type = ..Type(InternalName) + set imported = 1 if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { // Deployment manager should not reexport because studio project file includes timestamp @@ -1277,7 +1278,8 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A && $$$comClassDefined("%Studio.SourceControl.Production") && ##class(%Studio.SourceControl.Production).IsProductionClass( ..NameWithoutExtension(InternalName), "FullExternalName") { - set sc = ##class(%Studio.SourceControl.Production).ImportProductionWithoutDefinition(filename) + write !, "Production decomposition enabled, skipping import of production class" + set imported = 0 } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { set routineMgr = ##class(%RoutineMgr).%OpenId(InternalName) do routineMgr.Code.Rewind() @@ -1293,12 +1295,14 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set sc = $system.OBJ.Load(filename,"-l-d") } } - if sc { - set sc = ..UpdateRoutineTSH(InternalName, fileTSH) - if type = "prj" { - set sc = $$$ADDSC(sc, ..FixProjectCspReferences(InternalName)) + if $$$ISOK(sc) { + if imported { + set sc = ..UpdateRoutineTSH(InternalName, fileTSH) + if type = "prj" { + set sc = $$$ADDSC(sc, ..FixProjectCspReferences(InternalName)) + } + write !, InternalName," has been imported from ", filename } - write !, InternalName," has been imported from ", filename } else { write !, "ERROR importing" ,InternalName, ! do $system.Status.DisplayError(sc) @@ -1537,7 +1541,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As if (type = "ptd") { $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportPTD(InternalName,"FullExternalName")) } elseif (..ItemIsProductionToDecompose(InternalName, .productionName)) { - $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProductionWithoutDefinition(productionName,"FullExternalName")) + write !, "Production decomposition enabled, skipping export of production class" } else { $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) } @@ -1555,7 +1559,8 @@ ClassMethod ItemIsProductionToDecompose(InternalName, Output productionName) set settings = ##class(SourceControl.Git.Settings).%New() set name = $piece(InternalName,".",1,*-1) set decomposeProduction = settings.decomposeProductions && (..Type(InternalName) = "cls") - && $$$comClassDefined(name) && $classmethod(name, "%Extends","Ens.Production") + && $$$comClassDefined("%Studio.SourceControl.Production") + && ##class(%Studio.SourceControl.Production).IsProductionClass(name, "FullExternalName") if decomposeProduction { set productionName = name } @@ -2633,3 +2638,4 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status } } + From 85bb10531e28d03e233d9d0dc13824221de2812a Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 7 May 2024 14:49:04 -0400 Subject: [PATCH 21/79] Add configuration setting for production decomposition --- cls/SourceControl/Git/Settings.cls | 5 ++++- cls/SourceControl/Git/Utils.cls | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 6a4ce435..e7c03c6b 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -23,6 +23,9 @@ Property percentClassReplace As %String [ InitialExpression = {##class(SourceCon /// Git project settings are read-only in the web user interface Property settingsUIReadOnly As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).SettingsUIReadOnly()} ]; +/// Interoperability productions are source-controlled under separate files for each configuration item +Property decomposeProductions As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).DecomposeProductions()} ]; + /// Attribution: Git username for user ${username} Property gitUserName As %String(MAXLEN = 255) [ InitialExpression = {##class(SourceControl.Git.Utils).GitUserName()} ]; @@ -112,6 +115,7 @@ Method %Save() As %Status // update value of basicUserMode to reflect the updated setting for basicMode set ..userBasicMode = ##class(SourceControl.Git.Utils).UserBasicMode() + set @storage@("settings","decomposeProductions") = ..decomposeProductions kill @##class(SourceControl.Git.Utils).MappingsNode() merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings @@ -229,4 +233,3 @@ Method OnAfterConfigure() As %Boolean } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 4e55ff2f..e150ca28 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -72,6 +72,11 @@ ClassMethod SettingsUIReadOnly() As %Status [ CodeMode = expression ] $Get(@..#Storage@("settings","settingsUIReadOnly"), 0) } +ClassMethod DecomposeProductions() As %Boolean [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","decomposeProductions"), 0) +} + /// Returns the current (or previous) value of the flag. ClassMethod Locked(newFlagValue As %Boolean) As %Boolean { From 5579de76686f929fdb66deffbc55003be8411605 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 14 May 2024 11:02:07 -0400 Subject: [PATCH 22/79] Export decomposed production after save --- cls/SourceControl/Git/Extension.cls | 5 ++++ cls/SourceControl/Git/Utils.cls | 38 ++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 63085919..4eacd8b8 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -337,6 +337,11 @@ Method ExternalName(InternalName As %String) As %String quit ##class(SourceControl.Git.Utils).ExternalName(InternalName) } +ClassMethod FullExternalName(InternalName As %String) As %String +{ + quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) +} + Method IsReadOnly(InternalName As %String) As %Boolean { quit ##class(SourceControl.Git.Utils).Locked() diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index e150ca28..4e720cc5 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1485,7 +1485,12 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As }elseif (type = "csp") && ..IsCspFolder(InternalName) { $$$QuitOnError(..ExportRoutinesAux(InternalName , "/", 0, force, .filenames)) }else { - if ..IsTempFileOutdated(InternalName) || force { + // for now, if this is a production class and production decomposition is enabled, don't worry about timestamps. + if ..ItemIsProductionToDecompose(InternalName) { + write !, "exporting decomposed production ", InternalName + set name = $piece(InternalName,".",1,*-1) + $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProduction(name, "FullExternalName")) + } elseif ..IsTempFileOutdated(InternalName) || force { #dim filename as %String = ..FullExternalName(InternalName, .MappingExists) if (MappingExists = 0){ write "Did not find a matching mapping for """_InternalName_""". Skipping export." @@ -1509,6 +1514,18 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As quit $$$OK } +ClassMethod ItemIsProductionToDecompose(InternalName) +{ + set settings = ##class(SourceControl.Git.Settings).%New() + set name = $piece(InternalName,".",1,*-1) + set ext = $zconvert($piece(InternalName,".",*),"U") + return settings.decomposeProductions + && ((ext = "PTD") + || ((ext = "CLS") + && ##class(%Dictionary.ClassDefinition).%ExistsId(name) + && $classmethod(name, "%Extends","Ens.Production"))) +} + ClassMethod ExportProject(project As %String, force As %Boolean = 0, ByRef filenames) As %Status { #dim rs as %ResultSet = ##class(%ResultSet).%New("%Studio.Project:ProjectItemsList") @@ -2090,7 +2107,23 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S // If no specific mapping was specified (p=""), then return the whole csp filename; otherwise return the name without the mapped piece set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") - + } elseif usertype && (docclass = "Ens.Util.ProjectTextDocument") { + // If using sharded Production Exports, use PTD naming conventions + if default { + set nam = $replace(nam,"||","/") + } + set $ListBuild(productionDir, nam) = $ListFromString(nam, "/") + // Abbreviate "ProductionSettings" to "ProdStgs", "Settings" to "Stgs". + Set prefix = $Case($Extract(nam), "P":"ProdStgs-", "S":"Stgs-") + Set nam = prefix_$Piece(nam,"-",2,*) + set $ListBuild(itemName, itemClassName) = $ListFromString(nam, "|") + if $Get(itemClassName) '= "" { + // Prevent errors from exceeding max file length by hashing item Class name + set nam = productionDir_"/"_itemName_$zhex($zcrc(itemClassName,6)) + } else { + set nam = productionDir_"/"_nam + } + return $translate(found_$Replace(nam,".","_")_".xml","\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) if default{ @@ -2577,4 +2610,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status } } - From f3bd74dd9b1ffd6fe17d9755fb9b01ebc790a0be Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 17 May 2024 10:48:02 -0400 Subject: [PATCH 23/79] External name to internal name logic for PTD - most of this should be refactored into %Studio.SourceControl.Production later. --- cls/SourceControl/Git/File.cls | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index ba624fc0..38f16c10 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -35,8 +35,38 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } new %SourceControl //don't trigger source hooks with this test load to get the Name set sc=$system.OBJ.Load(ExternalName,"-d",,.outName,1) - if (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { + set itemIsPTD = 0 + if $data(outName) = 11 { + set key = $order(outName("")) + while (key '= "") { + if ($zconvert($piece(outName,".",*),"U") = "PTD") { + set itemIsPTD = 1 + quit + } + set key = $order(outName(key)) + } + } + if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { + // Special handling for Production Text Documents, used for Source Controlling individual Production settings + set file = $piece(ExternalName, "/", *) + set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(ExternalName) + set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") + set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) + set productionClass = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") + if $extract(file,1,9) = "ProdStgs-" { + set internalName = productionClass_"||ProductionSettings-"_productionClass_".PTD" + } else { + // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name + set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") + set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) + set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") + set itemName = settingsPTD.GetValueAt("/Item/@Name") + set internalName = productionClass_"||"_itemName_"|"_itemClass_".PTD" + } + } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName + } + if (internalName '= "") { set inst.InternalName = internalName $$$ThrowOnError(inst.%Save()) } From de56a873bfd21ad526c7ad3ee8283a0d36a1ee9c Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:00:16 -0400 Subject: [PATCH 24/79] Added default mapping for decomposed production items --- cls/SourceControl/Git/Utils.cls | 1 + 1 file changed, 1 insertion(+) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 4e720cc5..9c0f7d72 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2550,6 +2550,7 @@ ClassMethod SetDefaultMappings(mappingsNode As %String) set @mappingsNode@("CLS","UnitTest")="test/" set @mappingsNode@("INC","*")="inc/" set @mappingsNode@("MAC","*")="rtn/" + set @mappingsNode@("PTD","*")="ptd/" } ClassMethod PrintStreams(streams... As %Stream.FileCharacter) From 6c679c21eba4b3bdf95de157b5c4f502130e14f5 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 9 Jul 2024 17:03:51 -0400 Subject: [PATCH 25/79] work in progress - only export modified production items --- cls/SourceControl/Git/Extension.cls | 24 +++++++++++++++++++++- cls/SourceControl/Git/Utils.cls | 32 +++++++++++++++-------------- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 4eacd8b8..230c02e0 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -276,6 +276,25 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName) set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName) if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) { + // If this is a production class and production decomposition is enabled, call recursively on all modified production items. + if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { + do ##class(%Studio.SourceControl.Production).GetModifiedItemsAfterSave(InternalName, .productionItems) + set key = $order(productionItems("")) + while (key '= "") { + if productionItems(key) = "D" { + set itemFilename = ..ExternalName(key) + if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) && ##class(%File).Exists(itemFilename) { + $$$ThrowOnError(##class(SourceControl.Git.Change).AddDeletedToUncommitted(itemFilename, InternalName)) + $$$ThrowOnError(##class(SourceControl.Git.Utils).DeleteExternalFile(InternalName)) + } + } elseif '..IsInSourceControl(key) { + $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(key)) + } else { + $$$ThrowOnError(..OnAfterSave(key)) + } + set key = $order(productionItems(key)) + } + } set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName)) $$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName)) @@ -354,6 +373,10 @@ Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Bool { set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" + if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { + do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) + //TODO: if any modified items in this production class are edited by a different user, prohibit editing + } set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) if filename="" quit $$$OK @@ -408,4 +431,3 @@ Method AddToSourceControl(InternalName As %String, Description As %String = "") } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 9c0f7d72..77d52575 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -908,7 +908,8 @@ ClassMethod IsCspFolder(InternalName As %String) As %Boolean /// pkg -- package
/// prj -- project
/// csp -- csp-page or csp-folder. See IsCspFolder
-/// csp -- any static file from csp-folder +/// csp -- any static file from csp-folder
+/// ptd -- interoperability production configuration item ClassMethod Type(InternalName As %String) As %String { #dim extension as %String = $zconvert($piece(InternalName,".",$length(InternalName,".")),"L") @@ -1485,12 +1486,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As }elseif (type = "csp") && ..IsCspFolder(InternalName) { $$$QuitOnError(..ExportRoutinesAux(InternalName , "/", 0, force, .filenames)) }else { - // for now, if this is a production class and production decomposition is enabled, don't worry about timestamps. - if ..ItemIsProductionToDecompose(InternalName) { - write !, "exporting decomposed production ", InternalName - set name = $piece(InternalName,".",1,*-1) - $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProduction(name, "FullExternalName")) - } elseif ..IsTempFileOutdated(InternalName) || force { + if (type = "ptd") || ..IsTempFileOutdated(InternalName) || force { #dim filename as %String = ..FullExternalName(InternalName, .MappingExists) if (MappingExists = 0){ write "Did not find a matching mapping for """_InternalName_""". Skipping export." @@ -1504,7 +1500,13 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As } set filenames($I(filenames)) = filename write !, "exporting new version of ", InternalName, " to ", filename - $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) + if (type = "ptd") { + $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportPTD(InternalName,"FullExternalName")) + } elseif (..ItemIsProductionToDecompose(InternalName, .productionName)) { + $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProductionWithoutDefinition(productionName,"FullExternalName")) + } else { + $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) + } $$$QuitOnError(..UpdateRoutineTSH(InternalName, $h)) if '##class(SourceControl.Git.Change).IsUncommitted(filename) { $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "add", InternalName, $username, "", 1, "", "", 0)) @@ -1514,16 +1516,16 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As quit $$$OK } -ClassMethod ItemIsProductionToDecompose(InternalName) +ClassMethod ItemIsProductionToDecompose(InternalName, Output productionName) { set settings = ##class(SourceControl.Git.Settings).%New() set name = $piece(InternalName,".",1,*-1) - set ext = $zconvert($piece(InternalName,".",*),"U") - return settings.decomposeProductions - && ((ext = "PTD") - || ((ext = "CLS") - && ##class(%Dictionary.ClassDefinition).%ExistsId(name) - && $classmethod(name, "%Extends","Ens.Production"))) + set decomposeProduction = settings.decomposeProductions && (..Type(InternalName) = "cls") + && $$$comClassDefined(name) && $classmethod(name, "%Extends","Ens.Production") + if decomposeProduction { + set productionName = name + } + return decomposeProduction } ClassMethod ExportProject(project As %String, force As %Boolean = 0, ByRef filenames) As %Status From 771d1788b7e80d3641e2113c93a9cd5b60042d00 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 14 May 2024 11:02:07 -0400 Subject: [PATCH 26/79] Export decomposed production after save --- cls/SourceControl/Git/Extension.cls | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 230c02e0..603ed575 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -361,6 +361,11 @@ ClassMethod FullExternalName(InternalName As %String) As %String quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) } +ClassMethod FullExternalName(InternalName As %String) As %String +{ + quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) +} + Method IsReadOnly(InternalName As %String) As %Boolean { quit ##class(SourceControl.Git.Utils).Locked() From 5225402452bef1a5523de152129d5d5ec8a224ae Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 12 Jul 2024 13:24:57 -0400 Subject: [PATCH 27/79] remove method that got duplicated by bad resolve --- cls/SourceControl/Git/Extension.cls | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 603ed575..230c02e0 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -361,11 +361,6 @@ ClassMethod FullExternalName(InternalName As %String) As %String quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) } -ClassMethod FullExternalName(InternalName As %String) As %String -{ - quit ##class(SourceControl.Git.Utils).FullExternalName(InternalName) -} - Method IsReadOnly(InternalName As %String) As %Boolean { quit ##class(SourceControl.Git.Utils).Locked() From 82d18369f0124ac0edfeeb29988888b66af4f7ae Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 14:34:48 -0400 Subject: [PATCH 28/79] fix for add to source control because PTD internal name doesn't exist in database --- cls/SourceControl/Git/Utils.cls | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 77d52575..676d7e6e 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -661,7 +661,9 @@ ClassMethod AddToSourceControl(InternalName As %String) As %Status } for i=1:1:$Get(filenames) { - set FileInternalName = ##class(SourceControl.Git.Utils).NormalizeExtension(##class(SourceControl.Git.Utils).NameToInternalName(filenames(i), 0,,1)) + set ignoreNonexistent = (type '= "ptd") + set FileInternalName = ##class(SourceControl.Git.Utils).NormalizeExtension( + ##class(SourceControl.Git.Utils).NameToInternalName(filenames(i), 0,ignoreNonexistent,1)) if (FileInternalName = "") { continue } From 48ceba992008b8652bf98f4a46c26547d2ea0a38 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:42:45 -0400 Subject: [PATCH 29/79] more fixes to name logic for PTDs --- cls/SourceControl/Git/File.cls | 3 +-- cls/SourceControl/Git/Utils.cls | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 38f16c10..13a39d84 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -61,7 +61,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") set itemName = settingsPTD.GetValueAt("/Item/@Name") - set internalName = productionClass_"||"_itemName_"|"_itemClass_".PTD" + set internalName = productionClass_"||Settings-"_itemName_"|"_itemClass_".PTD" } } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName @@ -99,4 +99,3 @@ Storage Default } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 676d7e6e..9da1ea28 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1985,7 +1985,7 @@ ClassMethod GitStatus(ByRef files, IncludeAllFiles = 0) while $listnext(list, pointer, item) { set operation = $zstrip($extract(item, 1, 2), " Date: Thu, 18 Jul 2024 15:54:17 -0400 Subject: [PATCH 30/79] GetStatus logic to prohibit editing item checked out by other user --- cls/SourceControl/Git/Extension.cls | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 230c02e0..36073763 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -375,7 +375,13 @@ Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Bool set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) - //TODO: if any modified items in this production class are edited by a different user, prohibit editing + set key = $order(productionItems("")) + while (key '= "") { + // if any modified items in this production class are checked out by a different user, fail the check. + set st = ..GetStatus(key, .IsInSourceControl, .Editable, .IsCheckedOut, .UserCheckedOut) + if 'Editable return st + set key = $order(productionItems(key)) + } } set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) From a74b72abdde88bd15a3049a300cf515012ee6212 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:26:29 -0400 Subject: [PATCH 31/79] useful alert shown to user; remove spurious alert on loading config page if prod settings are edited by another user --- cls/SourceControl/Git/Extension.cls | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 36073763..854079f9 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -366,23 +366,35 @@ Method IsReadOnly(InternalName As %String) As %Boolean quit ##class(SourceControl.Git.Utils).Locked() } -/// Check the status of the given item -/// User should be able to edit the item if it is not locked by another user -/// Check if another user has committed any changes to the item and return the status -Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Boolean, ByRef Editable As %Boolean, ByRef IsCheckedOut As %Boolean, ByRef UserCheckedOut As %String) As %Status +/// Called before the item is saved to the database it is passed +/// a reference to the current temporary storage of this item so that it +/// can be modified before the save completes. If you quit with an error +/// value then it will abort the save. +Method OnBeforeSave(InternalName As %String, Location As %String = "", Object As %RegisteredObject = {$$$NULLOREF}) As %Status { - set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) - set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" + set st = $$$OK if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) set key = $order(productionItems("")) while (key '= "") { // if any modified items in this production class are checked out by a different user, fail the check. set st = ..GetStatus(key, .IsInSourceControl, .Editable, .IsCheckedOut, .UserCheckedOut) - if 'Editable return st + quit:$$$ISERR(st) + if 'Editable set st = $$$ERROR($$$GeneralError,"Item is checked out by another user: "_UserCheckedOut) + quit:$$$ISERR(st) set key = $order(productionItems(key)) } } + return st +} + +/// Check the status of the given item +/// User should be able to edit the item if it is not locked by another user +/// Check if another user has committed any changes to the item and return the status +Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Boolean, ByRef Editable As %Boolean, ByRef IsCheckedOut As %Boolean, ByRef UserCheckedOut As %String) As %Status +{ + set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) + set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) if filename="" quit $$$OK From 4ae9fcd44d4f6d1ad52314e331aebfddd5b63695 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 18 Jul 2024 17:04:05 -0400 Subject: [PATCH 32/79] import PTD uses deployment manager to apply setting --- cls/SourceControl/Git/Utils.cls | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 9da1ea28..623ce3ce 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1261,8 +1261,14 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A #dim fileTSH = ##class(%File).GetFileDateModified(filename) #dim sc as %Status = $$$OK - if ..IsRoutineOutdated(InternalName) || force { - if ..UserTypeCached(InternalName,.docclass,.doctype) { + set type = ..Type(InternalName) + if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ + if (type = "ptd") && $$$comClassDefined("Ens.Deployment.Deploy") { + set targetProduction = $piece(InternalName,"||",1) + set rollbackFile = ##class(%File).TempFilename() + set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) + do ##class(%File).Delete(rollbackFile) + } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { set routineMgr = ##class(%RoutineMgr).%OpenId(InternalName) do routineMgr.Code.Rewind() set source = ##class(%Stream.FileCharacter).%OpenId(filename,,.sc) @@ -1279,7 +1285,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A } if sc { set sc = ..UpdateRoutineTSH(InternalName, fileTSH) - if ..Type(InternalName) = "prj" { + if type = "prj" { set sc = $$$ADDSC(sc, ..FixProjectCspReferences(InternalName)) } write !, InternalName," has been imported from ", filename From e9fd8781d2146298133bfeead2a197b39f449e5f Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 23 Jul 2024 13:51:02 -0400 Subject: [PATCH 33/79] Refactored naming logic into production class --- cls/SourceControl/Git/File.cls | 17 +---------------- cls/SourceControl/Git/Utils.cls | 20 +++----------------- 2 files changed, 4 insertions(+), 33 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 13a39d84..292a8c33 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -47,22 +47,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } } if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - // Special handling for Production Text Documents, used for Source Controlling individual Production settings - set file = $piece(ExternalName, "/", *) - set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(ExternalName) - set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") - set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) - set productionClass = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") - if $extract(file,1,9) = "ProdStgs-" { - set internalName = productionClass_"||ProductionSettings-"_productionClass_".PTD" - } else { - // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name - set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") - set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) - set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") - set itemName = settingsPTD.GetValueAt("/Item/@Name") - set internalName = productionClass_"||Settings-"_itemName_"|"_itemClass_".PTD" - } + do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,,.internalName) } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName } diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 623ce3ce..ab2d46ed 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2117,23 +2117,9 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S // If no specific mapping was specified (p=""), then return the whole csp filename; otherwise return the name without the mapped piece set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") - } elseif usertype && (docclass = "Ens.Util.ProjectTextDocument") { - // If using sharded Production Exports, use PTD naming conventions - if default { - set nam = $replace(nam,"||","/") - } - set $ListBuild(productionDir, nam) = $ListFromString(nam, "/") - // Abbreviate "ProductionSettings" to "ProdStgs", "Settings" to "Stgs". - Set prefix = $Case($Extract(nam), "P":"ProdStgs-", "S":"Stgs-", :"") - Set nam = prefix_$Piece(nam,"-",2,*) - set $ListBuild(itemName, itemClassName) = $ListFromString(nam, "|") - if $Get(itemClassName) '= "" { - // Prevent errors from exceeding max file length by hashing item Class name - set nam = productionDir_"/"_itemName_$zhex($zcrc(itemClassName,6)) - } else { - set nam = productionDir_"/"_nam - } - return $translate(found_$Replace(nam,".","_")_".xml","\","/") + } elseif (..Type(InternalName) = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { + do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,,.filename) + return $translate(found_filename, "\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) if default{ From 7bf670f1bdca6d13071d14c3f2105f8ac77c2780 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 24 Jul 2024 11:19:35 -0400 Subject: [PATCH 34/79] remove unused argument on parseexternal/internalname --- cls/SourceControl/Git/File.cls | 2 +- cls/SourceControl/Git/Utils.cls | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 292a8c33..7a3ff455 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -47,7 +47,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } } if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,,.internalName) + do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,.internalName) } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName } diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index ab2d46ed..88515b0e 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2118,7 +2118,7 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") } elseif (..Type(InternalName) = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { - do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,,.filename) + do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,.filename) return $translate(found_filename, "\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) From 55d09865820e37eb2fc05ecb362658234284baf2 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:58:08 -0400 Subject: [PATCH 35/79] import all now imports user types in subdirectories recursively --- cls/SourceControl/Git/Utils.cls | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 88515b0e..d682ac52 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1316,6 +1316,24 @@ ClassMethod ImportCSPFile(InternalName As %String) As %Status Quit sc } +ClassMethod ListItemsRecursively(type, fileSpec, directory, ByRef itemList) [ Private ] +{ + set files = ##class(%Library.File).FileSetFunc(directory,fileSpec,,1) + throw:files.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(files.%SQLCODE,files.%Message) + while files.%Next() { + if (files.Type="D") { + do ..ListItemsRecursively(type, fileSpec, files.Name, .itemList) + } else { + set internalName = files.ItemName + if ($zconvert(type,"l") = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { + do ##class(%Studio.SourceControl.Production).ParseExternalName($translate(files.Name,"\","/"), .internalName) + } + set itemList(internalName) = "" + } + } +} + +/// Returns an array of internal names of all items in the local source control repository. ClassMethod ListItemsInFiles(ByRef itemList, ByRef err) As %Status { #define DoNotLoad 1 @@ -1332,13 +1350,11 @@ ClassMethod ListItemsInFiles(ByRef itemList, ByRef err) As %Status set mappedFilePath = ##class(%File).NormalizeFilename(mappedRelativePath, ..TempFolder()) if (##class(%File).DirectoryExists(mappedFilePath)){ - if ..UserTypeCached("foo."_mappingFileType) { - set fileSpec = "*."_$zcvt(mappingFileType,"L")_";*."_$zconvert(mappingFileType,"U") - set files = ##class(%Library.File).FileSetFunc(mappedFilePath,fileSpec) - while files.%Next() { - // Assumes flat file structure - set itemList(files.ItemName) = "" - } + if ..UserTypeCached("foo."_mappingFileType, .userTypeClass) { + set fileSpec = $select( + userTypeClass="Ens.Util.ProjectTextDocument": "*.xml;*.XML", + 1: "*."_$zcvt(mappingFileType,"L")_";*."_$zconvert(mappingFileType,"U")) + do ..ListItemsRecursively(mappingFileType, fileSpec, mappedFilePath, .itemList) } else { set res = $system.OBJ.ImportDir(mappedFilePath,,"-d",.err,1, .tempItemList, $$$DoNotLoad) merge itemList = tempItemList From cb1c167f7cac1ad0b71e4d8203d6f411cb16799a Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:14:32 -0400 Subject: [PATCH 36/79] import production class without definition --- cls/SourceControl/Git/Utils.cls | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index d682ac52..1fbf7c3d 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1261,6 +1261,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A #dim fileTSH = ##class(%File).GetFileDateModified(filename) #dim sc as %Status = $$$OK + set settings = ##class(SourceControl.Git.Settings).%New() set type = ..Type(InternalName) if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && $$$comClassDefined("Ens.Deployment.Deploy") { @@ -1268,6 +1269,11 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set rollbackFile = ##class(%File).TempFilename() set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) do ##class(%File).Delete(rollbackFile) + } elseif (type = "cls") && settings.decomposeProductions + && $$$comClassDefined("%Studio.SourceControl.Production") + && ##class(%Studio.SourceControl.Production).IsProductionClass( + ..NameWithoutExtension(InternalName), "FullExternalName") { + set sc = ##class(%Studio.SourceControl.Production).ImportProductionWithoutDefinition(filename) } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { set routineMgr = ##class(%RoutineMgr).%OpenId(InternalName) do routineMgr.Code.Rewind() From 4225957ad302aa8e5ba296a24e1e04fbbbda78cb Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:26:34 -0400 Subject: [PATCH 37/79] refactor: better way of determining if namespace has Ensemble --- cls/SourceControl/Git/Utils.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 1fbf7c3d..15a1302a 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1264,7 +1264,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set settings = ##class(SourceControl.Git.Settings).%New() set type = ..Type(InternalName) if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ - if (type = "ptd") && $$$comClassDefined("Ens.Deployment.Deploy") { + if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { set targetProduction = $piece(InternalName,"||",1) set rollbackFile = ##class(%File).TempFilename() set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) From e507af0ba0c3e10a2557aae52610e454e9b5af8c Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 25 Jul 2024 16:49:04 -0400 Subject: [PATCH 38/79] fix deleting production items deletes file from source control --- cls/SourceControl/Git/Extension.cls | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 854079f9..25b665b0 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -282,10 +282,10 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU set key = $order(productionItems("")) while (key '= "") { if productionItems(key) = "D" { - set itemFilename = ..ExternalName(key) - if ##class(SourceControl.Git.Utils).IsInSourceControl(InternalName) && ##class(%File).Exists(itemFilename) { - $$$ThrowOnError(##class(SourceControl.Git.Change).AddDeletedToUncommitted(itemFilename, InternalName)) - $$$ThrowOnError(##class(SourceControl.Git.Utils).DeleteExternalFile(InternalName)) + set itemFilename = ..FullExternalName(key) + if ##class(SourceControl.Git.Utils).IsInSourceControl(key) && ##class(%File).Exists(itemFilename) { + $$$ThrowOnError(##class(SourceControl.Git.Change).AddDeletedToUncommitted(itemFilename, key)) + $$$ThrowOnError(##class(SourceControl.Git.Utils).DeleteExternalFile(key)) } } elseif '..IsInSourceControl(key) { $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(key)) From 6bb25c31735bd17583b56bdb1596b76568aba627 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:11:32 -0400 Subject: [PATCH 39/79] fix issue where importing PTD items reexports them as changed --- cls/SourceControl/Git/Extension.cls | 1 + cls/SourceControl/Git/Utils.cls | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 25b665b0..f8299b9c 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -272,6 +272,7 @@ Method OnBeforeTimestamp(InternalName As %String) Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NULLOREF}) As %Status { set sc = $$$OK + quit:$get(%gscSkipSaveHooks) sc try { set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName) set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 15a1302a..65d0bd0a 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1265,6 +1265,10 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set type = ..Type(InternalName) if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { + // Deployment manager should not reexport because studio project file includes timestamp + // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl + new %gscSkipSaveHooks + set %gscSkipSaveHooks = 1 set targetProduction = $piece(InternalName,"||",1) set rollbackFile = ##class(%File).TempFilename() set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) From 5ab95d5ff2e33657f01074a3ec7d5e8570adbb5d Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:34:56 -0400 Subject: [PATCH 40/79] Production classes now excluded from export/import --- cls/SourceControl/Git/Utils.cls | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 65d0bd0a..40c4225e 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1263,6 +1263,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set settings = ##class(SourceControl.Git.Settings).%New() set type = ..Type(InternalName) + set imported = 1 if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { // Deployment manager should not reexport because studio project file includes timestamp @@ -1277,7 +1278,8 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A && $$$comClassDefined("%Studio.SourceControl.Production") && ##class(%Studio.SourceControl.Production).IsProductionClass( ..NameWithoutExtension(InternalName), "FullExternalName") { - set sc = ##class(%Studio.SourceControl.Production).ImportProductionWithoutDefinition(filename) + write !, "Production decomposition enabled, skipping import of production class" + set imported = 0 } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { set routineMgr = ##class(%RoutineMgr).%OpenId(InternalName) do routineMgr.Code.Rewind() @@ -1293,12 +1295,14 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set sc = $system.OBJ.Load(filename,"-l-d") } } - if sc { - set sc = ..UpdateRoutineTSH(InternalName, fileTSH) - if type = "prj" { - set sc = $$$ADDSC(sc, ..FixProjectCspReferences(InternalName)) + if $$$ISOK(sc) { + if imported { + set sc = ..UpdateRoutineTSH(InternalName, fileTSH) + if type = "prj" { + set sc = $$$ADDSC(sc, ..FixProjectCspReferences(InternalName)) + } + write !, InternalName," has been imported from ", filename } - write !, InternalName," has been imported from ", filename } else { write !, "ERROR importing" ,InternalName, ! do $system.Status.DisplayError(sc) @@ -1537,7 +1541,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As if (type = "ptd") { $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportPTD(InternalName,"FullExternalName")) } elseif (..ItemIsProductionToDecompose(InternalName, .productionName)) { - $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportProductionWithoutDefinition(productionName,"FullExternalName")) + write !, "Production decomposition enabled, skipping export of production class" } else { $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) } @@ -1555,7 +1559,8 @@ ClassMethod ItemIsProductionToDecompose(InternalName, Output productionName) set settings = ##class(SourceControl.Git.Settings).%New() set name = $piece(InternalName,".",1,*-1) set decomposeProduction = settings.decomposeProductions && (..Type(InternalName) = "cls") - && $$$comClassDefined(name) && $classmethod(name, "%Extends","Ens.Production") + && $$$comClassDefined("%Studio.SourceControl.Production") + && ##class(%Studio.SourceControl.Production).IsProductionClass(name, "FullExternalName") if decomposeProduction { set productionName = name } @@ -2633,3 +2638,4 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status } } + From 6e16c7dd75ac95b1d629cc7252f147744bd7846a Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 20 Aug 2024 10:33:15 -0400 Subject: [PATCH 41/79] production utilities moved to class in git-source-control --- cls/SourceControl/Git/Extension.cls | 5 +- cls/SourceControl/Git/File.cls | 3 +- cls/SourceControl/Git/Production.cls | 291 +++++++++++++++++++++++++++ cls/SourceControl/Git/Utils.cls | 16 +- 4 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 cls/SourceControl/Git/Production.cls diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index f8299b9c..aa84a637 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -279,7 +279,7 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) { // If this is a production class and production decomposition is enabled, call recursively on all modified production items. if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { - do ##class(%Studio.SourceControl.Production).GetModifiedItemsAfterSave(InternalName, .productionItems) + do ##class(SourceControl.Git.Production).GetModifiedItemsAfterSave(InternalName, .productionItems) set key = $order(productionItems("")) while (key '= "") { if productionItems(key) = "D" { @@ -375,7 +375,7 @@ Method OnBeforeSave(InternalName As %String, Location As %String = "", Object As { set st = $$$OK if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { - do ##class(%Studio.SourceControl.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) + do ##class(SourceControl.Git.Production).GetModifiedItemsBeforeSave(InternalName,,.productionItems) set key = $order(productionItems("")) while (key '= "") { // if any modified items in this production class are checked out by a different user, fail the check. @@ -450,3 +450,4 @@ Method AddToSourceControl(InternalName As %String, Description As %String = "") } } + diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 7a3ff455..3432fccf 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -47,7 +47,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } } if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - do ##class(%Studio.SourceControl.Production).ParseExternalName(ExternalName,.internalName) + do ##class(SourceControl.Git.Production).ParseExternalName(ExternalName,.internalName) } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName } @@ -84,3 +84,4 @@ Storage Default } } + diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls new file mode 100644 index 00000000..8de7763a --- /dev/null +++ b/cls/SourceControl/Git/Production.cls @@ -0,0 +1,291 @@ +/// This class serves as an intermediary for managing Source Control for Productions. +/// Included is handling for exporting Productions as individual pieces of settings +/// and importing individual item settings. +Class SourceControl.Git.Production Extends %RegisteredObject +{ + +/// Exports settings for a given Production and each Config Item from +/// the ProductionDefinition as separate XMLs. These are exported to +/// the /ptd subdirectory under the client's ^Sources directory. +ClassMethod ExportProductionDefinitionShards(productionClass As %String, nameMethod As %String) As %Status +{ + // First, export Production definition omitting Config Items + Set sc = ..ExportProductionSettings(productionClass, nameMethod) + If $$$ISERR(sc) { + Return sc + } + + // next, export each item to a separate file + Set rs = ##class(%SQL.Statement).%ExecDirect(, + "select Name, ClassName from Ens_Config.Item where Production = ?" + , productionClass + ) + Throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) + While rs.%Next() { + Set ptdName = "" + Set item = ##class(Ens.Config.Production).OpenItemByConfigName(productionClass _ "||" _ rs.Name _ "|" _ rs.ClassName) + If $isobject(item) { + Set sc = ..ExportConfigItemSettings(productionClass, item, nameMethod) + If $$$ISERR(sc) { + Return sc + } + } + } + Return $$$OK +} + +/// Exports a Studio project including both the provided PTD and export notes for the PTD +ClassMethod ExportProjectForPTD(productionClass, ptdName, exportPath) As %Status +{ + set st = $$$OK + try { + set project = ##class(%Studio.Project).%New() + set project.Name = $replace($replace(ptdName,".","_"),":","-") + kill projContentsList + set projContentsList(ptdName _ ".PTD") = "" + $$$ThrowOnError(##class(Ens.Deployment.Utils).CreateExportNotesPTD(project.Name,productionClass,,.projContentsList,0,.exportNotesPTDName)) + // strip items from export notes that break our diff + set st = ##class(Ens.Util.ProjectTextDocument).GetStream(.notesStream, exportNotesPTDName) + quit:$$$ISERR(st) + set newNotesStream = ##class(%Stream.GlobalCharacter).%New() + while 'notesStream.AtEnd { + set line = notesStream.ReadLine() + if $match(line, "^<(Machine|Instance|Namespace|Username)>.*") { + // remove these + } elseif $match(line, "^.*") { + // dummy timestamp for source control hooks to work properly + set st = newNotesStream.WriteLine("1841-01-01 00:00:00.000") + quit:$$$ISERR(st) + } else { + set st = newNotesStream.WriteLine(line) + quit:$$$ISERR(st) + } + } + do:##class(%RoutineMgr).Exists(exportNotesPTDName_".PTD") ##class(%RoutineMgr).Delete(exportNotesPTDName_".PTD") + set st = ##class(Ens.Util.ProjectTextDocument).Create(newNotesStream, exportNotesPTDName, "Export Notes for export "_project.Name) + quit:$$$ISERR(st) + // Internal/External naming logic relies on Export Notes being added to project first. If this is changed check for dependencies + do project.AddItem(exportNotesPTDName_".PTD") + do project.AddItem(ptdName_".PTD") + $$$ThrowOnError(project.%Save()) + set projContentsList(exportNotesPTDName_".PTD") = "" + set projContentsList(project.Name_".PRJ") = "" + $$$ThrowOnError($System.OBJ.Export(.projContentsList, exportPath, "/diffexport=1")) + } catch err { + set st = err.AsStatus() + } + if $IsObject(project) { + set st = $$$ADDSC(st,##class(%Studio.Project).%DeleteId(project.Name)) + } + return st +} + +/// Creates and exports a PTD item for a given internal name, either a single config item +/// or the production settings. +ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status +{ + Set name = $Piece(internalName,".",1,$Length(internalName,".")-1) + Set $ListBuild(productionName, itemName) = $ListFromString(name, "||") + Set $ListBuild(itemName, itemClassName) = $ListFromString(itemName, "|") + If $Piece($Piece(name,"||",2),"|",2) = "" { + Set sc = ..ExportProductionSettings(productionName, nameMethod) + } Else { + Set configItemName = productionName_"||"_$Piece(itemName, "Settings-", 2)_"|"_itemClassName + Set item = ##class(Ens.Config.Production).OpenItemByConfigName(configItemName) + Set sc = ..ExportConfigItemSettings(productionName, item, nameMethod) + } + Return sc +} + +/// Export a single Production Config Item. For a given Ens.Config.Item, the +/// exports the PTD for this item to the file system under the directory specified +ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String) As %Status +{ + Set internalName = productionClass_"||Settings-"_item.Name_"|"_item.ClassName_".PTD" + Set externalName = $ClassMethod($$SrcCtrlCls^%buildccr, nameMethod, internalName) + Set filename = ##class(%File).NormalizeFilename(externalName) + set st = ##class(Ens.Deployment.Utils).CreatePTDFromItem(.item, .ptdName) + $$$QuitOnError(st) + set st = ..ExportProjectForPTD(productionClass, ptdName, filename) + $$$QuitOnError(st) + Return st +} + +/// Exports the Production settings from ProductionDefinition given the Production +/// class name +ClassMethod ExportProductionSettings(productionClass As %String, nameMethod As %String) As %Status +{ + Set internalName = productionClass_"||ProductionSettings-"_productionClass_".PTD" + Set class = ##class(%Dictionary.CompiledClass).%OpenId(productionClass) + Set sc = ##class(Ens.Deployment.Utils).CreatePTDFromProduction(class, .ptdName) + If $$$ISERR(sc) { + Return sc + } + Set externalName = $ClassMethod($$SrcCtrlCls^%buildccr, nameMethod, internalName) + Set filename = ##class(%File).NormalizeFilename(externalName) + set sc = ..ExportProjectForPTD(productionClass, ptdName, filename) + Return sc +} + +ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedItems) +{ + kill modifiedItems + set productionName = $piece(internalName,".",1,*-1) + if ..IsEnsPortal() { + // If editing from SMP, get the modified items by looking at %IsModified on the items in the production in memory. + // No way to know if an item has been added or deleted, so ignore it. + set productionConfig = ##class(Ens.Config.Production).%OpenId(productionName) + if $isobject(productionConfig) { + set modifiedItem = $$$NULLOREF + for i=1:1:productionConfig.Items.Count() { + set item = productionConfig.Items.GetAt(i) + if item.%IsModified() { + set modifiedItem = item + quit + } + for j=1:1:item.Settings.Count() { + set setting = item.Settings.GetAt(i) + if $isobject(setting) && setting.%IsModified() { + set modifiedItem = item + quit + } + } + } + set modifiedInternalName = "" + if $isobject(modifiedItem) { + set modifiedInternalName = productionName _ "||Settings-" _ modifiedItem.Name _ "|" _ modifiedItem.ClassName _ ".PTD" + } else { + // cannot check %IsModified on production config settings because they are not actually modified at this point. + // workaround: just assume any change not to a specific item is to the production settings + set modifiedInternalName = productionName _ "||ProductionSettings-" _ productionName _ ".PTD" + } + } + if (modifiedInternalName '= "") { + set modifiedItems(modifiedInternalName) = "M" + } + } else { + // If editing/adding/deleting from Studio, get the modified items by comparing the XDATA in Location with the XDATA in the compiled class. + // FUTURE: implement this to support Studio + } + // populate data for use in OnAfterSave + kill ^mtempsscProd($job,"modifiedItems") + merge ^mtempsscProd($job,"modifiedItems") = modifiedItems + // FUTURE: use a percent variable or PPG instead + kill ^mtempsscProd($job,"items") + set rs = ##class(%SQL.Statement).%ExecDirect( + ,"select Name, ClassName from Ens_Config.Item where Production = ?" + , productionName) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + while rs.%Next() { + set ^mtempsscProd($job,"items",$listbuild(rs.Name, rs.ClassName)) = 1 + } +} + +ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) +{ + kill modifiedItems + set productionName = $piece(internalName,".",1,*-1) + if ..IsEnsPortal() { + // If adding/deleting from SMP, get the modified items by comparing items in temp global with items now + set rs = ##class(%SQL.Statement).%ExecDirect( + ,"select Name, ClassName from Ens_Config.Item where Production = ?" + , productionName) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + while rs.%Next() { + if '$get(^mtempsscProd($job,"items", $listbuild(rs.Name, rs.ClassName))) { + set itemInternalName = productionName _ "||Settings-" _ rs.Name _ "|" _ rs.ClassName _ ".PTD" + set modifiedItems(itemInternalName) = "A" + } + kill ^mtempsscProd($job,"items", $listbuild(rs.Name, rs.ClassName)) + } + set key = $order(^mtempsscProd($job,"items","")) + while (key '= "") { + set itemInternalName = productionName _ "||Settings-" _ $listget(key,1) _ "|" _ $listget(key,2) _ ".PTD" + set modifiedItems(itemInternalName) = "D" + set key = $order(^mtempsscProd($job,"items",key)) + } + // If editing from SMP, get the modified items from a cache stored in OnBeforeSave. + // Only do this if there are no added/deleted items, because otherwise production settings will be incorrectly included. + if '$data(modifiedItems) { + merge modifiedItems = ^mtempsscProd($job,"modifiedItems") + } + } else { + // If editing/adding/deleting from Studio, get the modified items from a percent variable set in OnBeforeSave. + // FUTURE: implement this to support Studio. + } +} + +/// Check if current CSP session is EnsPortal page +ClassMethod IsEnsPortal() As %Boolean +{ + If $IsObject($Get(%session)) && ($Get(%request.Data("pageclass","1")) [ "EnsPortal") { + Return 1 + } + Return 0 +} + +/// Perform check if Production Decomposition logic should be used for given item +ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %Boolean +{ + if $$$comClassDefined(className) { + return $classmethod(className, "%Extends", "Ens.Production") + } else { + set filename = $classmethod($$SrcCtrlCls^%buildccr, nameMethod, className_".CLS") + if ##class(%File).Exists(filename) { + $$$ThrowOnError($System.OBJ.Load(filename)) + } + set classDef = ##class(%Dictionary.ClassDefinition).%OpenId(className) + if $isobject(classDef) { + for key=1:1:classDef.XDatas.Count() { + if classDef.XDatas.GetAt(key).Name = "ProductionDefinition" { + return 1 + } + } + } + } + return 0 +} + +/// Given a file name for a PTD item, returns a suggested internal name. +ClassMethod ParseExternalName(externalName, Output internalName) +{ + set file = $piece(externalName, "/", *) + set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(externalName) + set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") + set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) + set productionName = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") + if $extract(file,1,9) = "ProdStgs-" { + set internalName = productionName_"||ProductionSettings-"_productionName_".PTD" + } else { + // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name + set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") + set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) + set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") + set itemName = settingsPTD.GetValueAt("/Item/@Name") + set internalName = productionName_"||Settings-"_itemName_"|"_itemClass_".PTD" + } +} + +/// Given an internal name for a PTD item, returns a suggested filename for export. +ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fileName) +{ + set name = $piece(internalName,".",1,*-1) + if 'noFolders { + set name = $replace(name,"||","/") + set $ListBuild(productionName, name) = $ListFromString(name, "/") + } + // Abbreviate "ProductionSettings" to "ProdStgs", "Settings" to "Stgs". + Set prefix = $Case($Extract(name), "P":"ProdStgs-", "S":"Stgs-", :"") + Set name = prefix_$Piece(name,"-",2,*) + set $ListBuild(itemName, itemClassName) = $ListFromString(name, "|") + set name = $select( + $get(itemClassName) '= "": itemName_$zhex($zcrc(itemClassName,6)), + 1: name + ) + if 'noFolders { + set name = productionName _ "/" _ name + } + set fileName = $translate($replace(name, ".", "_") _ ".xml", "\", "/") +} + +} diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 40c4225e..b51acfb2 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1275,8 +1275,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) do ##class(%File).Delete(rollbackFile) } elseif (type = "cls") && settings.decomposeProductions - && $$$comClassDefined("%Studio.SourceControl.Production") - && ##class(%Studio.SourceControl.Production).IsProductionClass( + && ##class(SourceControl.Git.Production).IsProductionClass( ..NameWithoutExtension(InternalName), "FullExternalName") { write !, "Production decomposition enabled, skipping import of production class" set imported = 0 @@ -1339,8 +1338,8 @@ ClassMethod ListItemsRecursively(type, fileSpec, directory, ByRef itemList) [ Pr do ..ListItemsRecursively(type, fileSpec, files.Name, .itemList) } else { set internalName = files.ItemName - if ($zconvert(type,"l") = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { - do ##class(%Studio.SourceControl.Production).ParseExternalName($translate(files.Name,"\","/"), .internalName) + if ($zconvert(type,"l") = "ptd") { + do ##class(SourceControl.Git.Production).ParseExternalName($translate(files.Name,"\","/"), .internalName) } set itemList(internalName) = "" } @@ -1539,7 +1538,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As set filenames($I(filenames)) = filename write !, "exporting new version of ", InternalName, " to ", filename if (type = "ptd") { - $$$QuitOnError(##class(%Studio.SourceControl.Production).ExportPTD(InternalName,"FullExternalName")) + $$$QuitOnError(##class(SourceControl.Git.Production).ExportPTD(InternalName,"FullExternalName")) } elseif (..ItemIsProductionToDecompose(InternalName, .productionName)) { write !, "Production decomposition enabled, skipping export of production class" } else { @@ -1559,8 +1558,7 @@ ClassMethod ItemIsProductionToDecompose(InternalName, Output productionName) set settings = ##class(SourceControl.Git.Settings).%New() set name = $piece(InternalName,".",1,*-1) set decomposeProduction = settings.decomposeProductions && (..Type(InternalName) = "cls") - && $$$comClassDefined("%Studio.SourceControl.Production") - && ##class(%Studio.SourceControl.Production).IsProductionClass(name, "FullExternalName") + && ##class(SourceControl.Git.Production).IsProductionClass(name, "FullExternalName") if decomposeProduction { set productionName = name } @@ -2148,8 +2146,8 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S // If no specific mapping was specified (p=""), then return the whole csp filename; otherwise return the name without the mapped piece set InternalName=$extract(InternalName,$length(p)+2,*) quit $translate(found_$translate(InternalName,"%","_"),"\","/") - } elseif (..Type(InternalName) = "ptd") && $$$comClassDefined("%Studio.SourceControl.Production") { - do ##class(%Studio.SourceControl.Production).ParseInternalName(InternalName,'default,.filename) + } elseif (..Type(InternalName) = "ptd") { + do ##class(SourceControl.Git.Production).ParseInternalName(InternalName,'default,.filename) return $translate(found_filename, "\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) From 098a708758f6d0a5fb7c69eb7a0bfebf61108f75 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:44:12 -0400 Subject: [PATCH 42/79] fix: production items are exported for source control when production is not in source control required now that production class is not a separate source controlled item --- cls/SourceControl/Git/Extension.cls | 14 ++++++++------ cls/SourceControl/Git/Utils.cls | 11 +++++++---- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 59bc8eae..a4828665 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -276,7 +276,7 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU try { set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(.InternalName) set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(InternalName) - if ##class(SourceControl.Git.Utils).IsNamespaceInGit() && ..IsInSourceControl(InternalName) { + if ##class(SourceControl.Git.Utils).IsNamespaceInGit() { // If this is a production class and production decomposition is enabled, call recursively on all modified production items. if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(InternalName) { do ##class(SourceControl.Git.Production).GetModifiedItemsAfterSave(InternalName, .productionItems) @@ -296,11 +296,13 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU set key = $order(productionItems(key)) } } - set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) - $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName)) - $$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName)) - if '##class(SourceControl.Git.Change).IsUncommitted(filename) { - $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0)) + if ..IsInSourceControl(InternalName) { + set filename = ##class(SourceControl.Git.Utils).FullExternalName(InternalName) + $$$ThrowOnError(##class(SourceControl.Git.Utils).RemoveRoutineTSH(InternalName)) + $$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName)) + if '##class(SourceControl.Git.Change).IsUncommitted(filename) { + $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0)) + } } } } catch e { diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 05914524..222dd28b 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1534,18 +1534,21 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As write "Mapping to another database found. Skipping export" quit $$$OK } - set filenames($I(filenames)) = filename write !, "exporting new version of ", InternalName, " to ", filename if (type = "ptd") { $$$QuitOnError(##class(SourceControl.Git.Production).ExportPTD(InternalName,"FullExternalName")) } elseif (..ItemIsProductionToDecompose(InternalName, .productionName)) { write !, "Production decomposition enabled, skipping export of production class" + set filename = "" } else { $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) } - $$$QuitOnError(..UpdateRoutineTSH(InternalName, $h)) - if '##class(SourceControl.Git.Change).IsUncommitted(filename) { - $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "add", InternalName, $username, "", 1, "", "", 0)) + if (filename '= "") { + set filenames($I(filenames)) = filename + $$$QuitOnError(..UpdateRoutineTSH(InternalName, $h)) + if '##class(SourceControl.Git.Change).IsUncommitted(filename) { + $$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "add", InternalName, $username, "", 1, "", "", 0)) + } } } } From 01d0759c4e2b00255e295c3e12899025ea16462e Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:09:42 -0400 Subject: [PATCH 43/79] deletes of PTD items now delete associated config item from production not fully working because deleting items with git pull does not work --- cls/SourceControl/Git/Production.cls | 36 ++++++++++++++++--- .../Git/PullEventHandler/IncrementalLoad.cls | 7 +++- cls/SourceControl/Git/Utils.cls | 3 +- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 8de7763a..53820374 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -266,8 +266,11 @@ ClassMethod ParseExternalName(externalName, Output internalName) } } -/// Given an internal name for a PTD item, returns a suggested filename for export. -ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fileName) +/// Given an internal name for a PTD item, returns a suggested filename for export, as well as: +/// - itemName: name of the configuration item +/// - productionName: name of the associated production +/// - isProdSettings: if true, this item is a production settings; if false, this item is a configuration item settings +ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fileName, Output itemName, Output productionName, Output isProdSettings As %Boolean) { set name = $piece(internalName,".",1,*-1) if 'noFolders { @@ -276,9 +279,10 @@ ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fi } // Abbreviate "ProductionSettings" to "ProdStgs", "Settings" to "Stgs". Set prefix = $Case($Extract(name), "P":"ProdStgs-", "S":"Stgs-", :"") - Set name = prefix_$Piece(name,"-",2,*) + set isProdSettings = ($Extract(name) = "P") + Set name = $Piece(name,"-",2,*) set $ListBuild(itemName, itemClassName) = $ListFromString(name, "|") - set name = $select( + set name = prefix_$select( $get(itemClassName) '= "": itemName_$zhex($zcrc(itemClassName,6)), 1: name ) @@ -288,4 +292,28 @@ ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fi set fileName = $translate($replace(name, ".", "_") _ ".xml", "\", "/") } +/// Given an internal name for a PTD item, removes that item from the production. +ClassMethod RemoveItem(internalName, noFolders As %Boolean = 0) As %Status +{ + set sc = $$$OK + try { + if '##class(%Library.EnsembleMgr).IsEnsembleNamespace() { + quit + } + do ..ParseInternalName(internalName, noFolders, , .itemName, .productionName, .isProdSettings) + if 'isProdSettings { + set production = ##class(Ens.Config.Production).%OpenId(productionName,,.sc) + quit:$$$ISERR(sc) + set configItem = production.OpenItemByConfigName(itemName,.sc) + quit:$$$ISERR(sc) + do production.RemoveItem(configItem) + set sc = production.%Save() + quit:$$$ISERR(sc) + } + } catch err { + set sc = err.AsStatus() + } + return sc +} + } diff --git a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls index 8dd64d3b..d7e06ee8 100644 --- a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -43,6 +43,7 @@ Method DeleteFile(item As %String) As %Status set sc = $$$OK set type = ##class(SourceControl.Git.Utils).Type(item) set name = ##class(SourceControl.Git.Utils).NameWithoutExtension(item) + set settings = ##class(SourceControl.Git.Settings).%New() set deleted = 1 if type = "prj" { set sc = $system.OBJ.DeleteProject(name) @@ -52,6 +53,11 @@ Method DeleteFile(item As %String) As %Status set sc = ##class(%Routine).Delete(item) }elseif type = "csp" { set sc = $System.CSP.DeletePage(item) + } elseif settings.decomposeProductions && (type = "ptd") { + set sc = ##class(%SYSTEM.Status).AppendStatus( + ##class(SourceControl.Git.Production).RemoveItem(item), + ##class(%Library.RoutineMgr).Delete(item) + ) }elseif ##class(SourceControl.Git.Utils).UserTypeCached(item) { set sc = ##class(%Library.RoutineMgr).Delete(item) } else { @@ -71,4 +77,3 @@ Method DeleteFile(item As %String) As %Status } } - diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 222dd28b..29d73e6d 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1264,7 +1264,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set type = ..Type(InternalName) set imported = 1 if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ - if (type = "ptd") && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { + if (type = "ptd") && settings.decomposeProductions && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { // Deployment manager should not reexport because studio project file includes timestamp // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl new %gscSkipSaveHooks @@ -2682,4 +2682,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status } } - From 9024dac5221d200506cef533f84f3a7c0d53b42c Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:28:11 -0400 Subject: [PATCH 44/79] fix: git pull now successfully deletes production config items --- .../Git/Util/ProductionItemCache.cls | 26 +++++++++++++ cls/SourceControl/Git/Utils.cls | 37 ++++++++++++------- 2 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 cls/SourceControl/Git/Util/ProductionItemCache.cls diff --git a/cls/SourceControl/Git/Util/ProductionItemCache.cls b/cls/SourceControl/Git/Util/ProductionItemCache.cls new file mode 100644 index 00000000..06d66bb0 --- /dev/null +++ b/cls/SourceControl/Git/Util/ProductionItemCache.cls @@ -0,0 +1,26 @@ +/// Maintains a cache mapping PTD items' full external names to internal names. +/// This is only required to deploy deletes because once the file is deleted from disk, there is +/// no way to determine the internal name for it. +Class SourceControl.Git.Util.ProductionItemCache +{ + +ClassMethod Store(externalName, internalName) +{ + if externalName '= "" { + set @##class(SourceControl.Git.Utils).#Storage@("ptd_items", $translate(externalName,"\","/")) = internalName + } +} + +ClassMethod Lookup(externalName) +{ + return $select( + externalName="":"", + 1:$get(@##class(SourceControl.Git.Utils).#Storage@("ptd_items", $translate(externalName,"\","/")))) +} + +ClassMethod Clear() +{ + kill @##class(SourceControl.Git.Utils).#Storage@("ptd_items") +} + +} diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 29d73e6d..c60c69a3 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1265,14 +1265,16 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set imported = 1 if ..IsRoutineOutdated(InternalName) || force || (type = "ptd"){ if (type = "ptd") && settings.decomposeProductions && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - // Deployment manager should not reexport because studio project file includes timestamp - // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl - new %gscSkipSaveHooks - set %gscSkipSaveHooks = 1 - set targetProduction = $piece(InternalName,"||",1) - set rollbackFile = ##class(%File).TempFilename() - set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) - do ##class(%File).Delete(rollbackFile) + if ##class(%File).Exists(filename) { + // Deployment manager should not reexport because studio project file includes timestamp + // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl + new %gscSkipSaveHooks + set %gscSkipSaveHooks = 1 + set targetProduction = $piece(InternalName,"||",1) + set rollbackFile = ##class(%File).TempFilename() + set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) + do ##class(%File).Delete(rollbackFile) + } } elseif (type = "cls") && settings.decomposeProductions && ##class(SourceControl.Git.Production).IsProductionClass( ..NameWithoutExtension(InternalName), "FullExternalName") { @@ -2150,7 +2152,9 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S quit $translate(found_$translate(InternalName,"%","_"),"\","/") } elseif (..Type(InternalName) = "ptd") { do ##class(SourceControl.Git.Production).ParseInternalName(InternalName,'default,.filename) - return $translate(found_filename, "\","/") + set externalName = $translate(found_filename, "\","/") + do ##class(SourceControl.Git.Util.ProductionItemCache).Store(..TempFolder()_externalName, InternalName) + return externalName } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) if default{ @@ -2216,10 +2220,6 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V } if (##class(%File).Exists(Name)) { set InternalName = ##class(SourceControl.Git.File).ExternalNameToInternalName(Name) - if (InternalName '= "") && (context.IsInGitEnabledPackage) { - // Don't need mappings! - return ..NormalizeInternalName(InternalName) - } } else { // check for file in uncommitted queue &sql(SELECT internalName into :InternalName FROM SourceControl_Git.Change where ItemFile = :Name) @@ -2229,6 +2229,11 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V set Deleted = 1 } } + // check for file in production item cache + if InternalName = "" { + set InternalName = ##class(SourceControl.Git.Util.ProductionItemCache).Lookup(Name) + } + // use mappings if (InternalName="") { set name=$extract(Name,$length($$$SourceRoot)+1,*) set name=$replace(name,"\","/") // standardize slash direction @@ -2361,7 +2366,11 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V } if ((IgnorePercent)&&($extract(InternalName)="%")) { set InternalName = "" } // don't return a result for % items if instructed to ignore them if ((IgnoreNonexistent)&&('##class(%RoutineMgr).Exists(InternalName))&&('Deleted)) { set InternalName = "" } // only return item names which exist in the DB - quit ..NormalizeInternalName(InternalName) + set normalizedInternalName = ..NormalizeInternalName(InternalName) + if ..Type(normalizedInternalName) = "ptd" { + do ##class(SourceControl.Git.Util.ProductionItemCache).Store(Name, normalizedInternalName) + } + quit normalizedInternalName } ClassMethod OutputConfigureMessage() From 1c3bb4d72f5e4555703db36ed6b7ab5230d940aa Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:47:17 -0400 Subject: [PATCH 45/79] port CreateProduction from CCR --- cls/SourceControl/Git/Production.cls | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 53820374..44125a12 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -227,7 +227,7 @@ ClassMethod IsEnsPortal() As %Boolean /// Perform check if Production Decomposition logic should be used for given item ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %Boolean { - if $$$comClassDefined(className) { + if (className '= "") && $$$comClassDefined(className) { return $classmethod(className, "%Extends", "Ens.Production") } else { set filename = $classmethod($$SrcCtrlCls^%buildccr, nameMethod, className_".CLS") @@ -235,12 +235,8 @@ ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %B $$$ThrowOnError($System.OBJ.Load(filename)) } set classDef = ##class(%Dictionary.ClassDefinition).%OpenId(className) - if $isobject(classDef) { - for key=1:1:classDef.XDatas.Count() { - if classDef.XDatas.GetAt(key).Name = "ProductionDefinition" { - return 1 - } - } + if $isobject(classDef) && ##class(%Dictionary.ClassDefinition).%ExistsId(classDef.Super) { + return $classmethod(classDef.Super, "%Extends", "Ens.Production") } } return 0 @@ -316,4 +312,26 @@ ClassMethod RemoveItem(internalName, noFolders As %Boolean = 0) As %Status return sc } +/// Given internal name for a Production Settings PTD, creates the corresponding Production +/// Class if it does not already exist in this namespace +ClassMethod CreateProduction(productionName As %String, superClasses = "") As %Status +{ + set classDef = ##class(%Dictionary.ClassDefinition).%New(productionName) + if superClasses '= "" { + set classDef.Super = superClasses + } else { + set classDef.Super = "Ens.Production" + } + set productionXData = ##class(%Dictionary.XDataDefinition).%New() + set productionXData.Name = "ProductionDefinition" + set sc = productionXData.Data.WriteLine("") + if $$$ISERR(sc) return sc + set sc = classDef.XDatas.Insert(productionXData) + if $$$ISERR(sc) return sc + set sc = classDef.%Save() + if $$$ISERR(sc) return sc + set sc = $System.OBJ.Compile(productionName) + return sc +} + } From dd221fcb80144b508172fa5596d1527393a26e48 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:04:02 -0400 Subject: [PATCH 46/79] deploying production items will create production class if it does not already exist --- cls/SourceControl/Git/Utils.cls | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index c60c69a3..4bbd9462 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1270,10 +1270,15 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl new %gscSkipSaveHooks set %gscSkipSaveHooks = 1 - set targetProduction = $piece(InternalName,"||",1) - set rollbackFile = ##class(%File).TempFilename() - set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) - do ##class(%File).Delete(rollbackFile) + do ##class(SourceControl.Git.Production).ParseInternalName(InternalName,,,,.targetProduction) + if (targetProduction '= "") && '$$$comClassDefined(targetProduction) { + set sc = ##class(SourceControl.Git.Production).CreateProduction(targetProduction) + } + if $$$ISOK(sc) { + set rollbackFile = ##class(%File).TempFilename() + set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) + do ##class(%File).Delete(rollbackFile) + } } } elseif (type = "cls") && settings.decomposeProductions && ##class(SourceControl.Git.Production).IsProductionClass( From 2398ac03222adee888e90e23f88acb8a56e27f15 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:51:52 -0400 Subject: [PATCH 47/79] Revert "fix: git pull now successfully deletes production config items" This reverts commit 9024dac5221d200506cef533f84f3a7c0d53b42c. --- .../Git/Util/ProductionItemCache.cls | 26 ------------------- cls/SourceControl/Git/Utils.cls | 19 +++++--------- 2 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 cls/SourceControl/Git/Util/ProductionItemCache.cls diff --git a/cls/SourceControl/Git/Util/ProductionItemCache.cls b/cls/SourceControl/Git/Util/ProductionItemCache.cls deleted file mode 100644 index 06d66bb0..00000000 --- a/cls/SourceControl/Git/Util/ProductionItemCache.cls +++ /dev/null @@ -1,26 +0,0 @@ -/// Maintains a cache mapping PTD items' full external names to internal names. -/// This is only required to deploy deletes because once the file is deleted from disk, there is -/// no way to determine the internal name for it. -Class SourceControl.Git.Util.ProductionItemCache -{ - -ClassMethod Store(externalName, internalName) -{ - if externalName '= "" { - set @##class(SourceControl.Git.Utils).#Storage@("ptd_items", $translate(externalName,"\","/")) = internalName - } -} - -ClassMethod Lookup(externalName) -{ - return $select( - externalName="":"", - 1:$get(@##class(SourceControl.Git.Utils).#Storage@("ptd_items", $translate(externalName,"\","/")))) -} - -ClassMethod Clear() -{ - kill @##class(SourceControl.Git.Utils).#Storage@("ptd_items") -} - -} diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 4bbd9462..2dbad1a3 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2157,9 +2157,7 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S quit $translate(found_$translate(InternalName,"%","_"),"\","/") } elseif (..Type(InternalName) = "ptd") { do ##class(SourceControl.Git.Production).ParseInternalName(InternalName,'default,.filename) - set externalName = $translate(found_filename, "\","/") - do ##class(SourceControl.Git.Util.ProductionItemCache).Store(..TempFolder()_externalName, InternalName) - return externalName + return $translate(found_filename, "\","/") } elseif ext="CLS"||(ext="PRJ")||usertype { set nam=$replace(nam,"%", ..PercentClassReplace()) if default{ @@ -2225,6 +2223,10 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V } if (##class(%File).Exists(Name)) { set InternalName = ##class(SourceControl.Git.File).ExternalNameToInternalName(Name) + if (InternalName '= "") && (context.IsInGitEnabledPackage) { + // Don't need mappings! + return ..NormalizeInternalName(InternalName) + } } else { // check for file in uncommitted queue &sql(SELECT internalName into :InternalName FROM SourceControl_Git.Change where ItemFile = :Name) @@ -2234,11 +2236,6 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V set Deleted = 1 } } - // check for file in production item cache - if InternalName = "" { - set InternalName = ##class(SourceControl.Git.Util.ProductionItemCache).Lookup(Name) - } - // use mappings if (InternalName="") { set name=$extract(Name,$length($$$SourceRoot)+1,*) set name=$replace(name,"\","/") // standardize slash direction @@ -2371,11 +2368,7 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V } if ((IgnorePercent)&&($extract(InternalName)="%")) { set InternalName = "" } // don't return a result for % items if instructed to ignore them if ((IgnoreNonexistent)&&('##class(%RoutineMgr).Exists(InternalName))&&('Deleted)) { set InternalName = "" } // only return item names which exist in the DB - set normalizedInternalName = ..NormalizeInternalName(InternalName) - if ..Type(normalizedInternalName) = "ptd" { - do ##class(SourceControl.Git.Util.ProductionItemCache).Store(Name, normalizedInternalName) - } - quit normalizedInternalName + quit ..NormalizeInternalName(InternalName) } ClassMethod OutputConfigureMessage() From 0c469ba38bca65a297b3776ef818e22817831257 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 4 Sep 2024 11:59:15 -0400 Subject: [PATCH 48/79] deployment deletes production items determines if item is PTD based on external name --- cls/SourceControl/Git/Production.cls | 76 +++++++++++++------ .../Git/PullEventHandler/IncrementalLoad.cls | 37 +++++++-- .../Git/PullEventHandler/IncrementalLoad.cls | 34 +++++++++ 3 files changed, 119 insertions(+), 28 deletions(-) create mode 100644 test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 44125a12..1e359edf 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -101,7 +101,7 @@ ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status /// exports the PTD for this item to the file system under the directory specified ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String) As %Status { - Set internalName = productionClass_"||Settings-"_item.Name_"|"_item.ClassName_".PTD" + set internalName = ..CreateInternalName(productionClass, item.Name, item.ClassName, 0) Set externalName = $ClassMethod($$SrcCtrlCls^%buildccr, nameMethod, internalName) Set filename = ##class(%File).NormalizeFilename(externalName) set st = ##class(Ens.Deployment.Utils).CreatePTDFromItem(.item, .ptdName) @@ -115,7 +115,7 @@ ClassMethod ExportConfigItemSettings(productionClass As %String, item As %Regist /// class name ClassMethod ExportProductionSettings(productionClass As %String, nameMethod As %String) As %Status { - Set internalName = productionClass_"||ProductionSettings-"_productionClass_".PTD" + set internalName = ..CreateInternalName(productionClass,,,0) Set class = ##class(%Dictionary.CompiledClass).%OpenId(productionClass) Set sc = ##class(Ens.Deployment.Utils).CreatePTDFromProduction(class, .ptdName) If $$$ISERR(sc) { @@ -153,11 +153,11 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt } set modifiedInternalName = "" if $isobject(modifiedItem) { - set modifiedInternalName = productionName _ "||Settings-" _ modifiedItem.Name _ "|" _ modifiedItem.ClassName _ ".PTD" + set modifiedInternalName = ..CreateInternalName(productionName, modifiedItem.Name, modifiedItem.ClassName, 0) } else { // cannot check %IsModified on production config settings because they are not actually modified at this point. // workaround: just assume any change not to a specific item is to the production settings - set modifiedInternalName = productionName _ "||ProductionSettings-" _ productionName _ ".PTD" + set modifiedInternalName = ..CreateInternalName(productionName,,,1) } } if (modifiedInternalName '= "") { @@ -193,14 +193,14 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) while rs.%Next() { if '$get(^mtempsscProd($job,"items", $listbuild(rs.Name, rs.ClassName))) { - set itemInternalName = productionName _ "||Settings-" _ rs.Name _ "|" _ rs.ClassName _ ".PTD" + set itemInternalName = ..CreateInternalName(productionName, rs.Name, rs.ClassName, 0) set modifiedItems(itemInternalName) = "A" } kill ^mtempsscProd($job,"items", $listbuild(rs.Name, rs.ClassName)) } set key = $order(^mtempsscProd($job,"items","")) while (key '= "") { - set itemInternalName = productionName _ "||Settings-" _ $listget(key,1) _ "|" _ $listget(key,2) _ ".PTD" + set itemInternalName = ..CreateInternalName(productionName, $listget(key,1), $listget(key,2), 0) set modifiedItems(itemInternalName) = "D" set key = $order(^mtempsscProd($job,"items",key)) } @@ -242,23 +242,25 @@ ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %B return 0 } -/// Given a file name for a PTD item, returns a suggested internal name. -ClassMethod ParseExternalName(externalName, Output internalName) +/// Given a file name for a PTD item, returns a suggested internal name. This method assumes that the file exists on disk. +ClassMethod ParseExternalName(externalName, Output internalName = "", Output productionName = "") { - set file = $piece(externalName, "/", *) - set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(externalName) - set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") - set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) - set productionName = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") - if $extract(file,1,9) = "ProdStgs-" { - set internalName = productionName_"||ProductionSettings-"_productionName_".PTD" - } else { - // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name - set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") - set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) - set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") - set itemName = settingsPTD.GetValueAt("/Item/@Name") - set internalName = productionName_"||Settings-"_itemName_"|"_itemClass_".PTD" + if ##class(%File).Exists(externalName) { + set file = $piece(externalName, "/", *) + set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(externalName) + set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") + set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) + set productionName = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") + if $extract(file,1,9) = "ProdStgs-" { + set internalName = ..CreateInternalName(productionName,,,1) + } else { + // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name + set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") + set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) + set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") + set itemName = settingsPTD.GetValueAt("/Item/@Name") + set internalName = ..CreateInternalName(productionName, itemName, itemClass, 0) + } } } @@ -288,6 +290,36 @@ ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fi set fileName = $translate($replace(name, ".", "_") _ ".xml", "\", "/") } +/// Calculates the internal name for a decomposed production item +ClassMethod CreateInternalName(productionName = "", itemName = "", itemClassName = "", isProductionSettings As %Boolean = 0) +{ + return $select( + isProductionSettings: productionName_"||ProductionSettings-"_productionName_".PTD", + 1: productionName _ "||Settings-" _ itemName _ "|" _ itemClassName _ ".PTD" + ) +} + +/// Given an external name for a PTD item, removes that item from the production. +ClassMethod RemoveItemByExternalName(externalName, nameMethod) As %Status +{ + set sc = $$$OK + set productionName = $replace($piece($replace(externalName,"\","/"),"/",*-1),"_",".") + set production = ##class(Ens.Config.Production).%OpenId(productionName,,.sc) + $$$QuitOnError(sc) + set itemToRemove = $$$NULLOREF + for i=1:1:production.Items.Count() { + set configItem = production.Items.GetAt(i) + set itemInternalName = ..CreateInternalName(productionName, configItem.Name, configItem.ClassName) + set itemExternalName = $classmethod($$SrcCtrlCls^%buildccr, nameMethod, itemInternalName) + if itemExternalName = externalName { + set itemToRemove = configItem + quit + } + } + do production.RemoveItem(itemToRemove) + return production.%Save() +} + /// Given an internal name for a PTD item, removes that item from the production. ClassMethod RemoveItem(internalName, noFolders As %Boolean = 0) As %Status { diff --git a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls index d7e06ee8..0b9ac56e 100644 --- a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -17,7 +17,7 @@ Method OnPull() As %Status if ((internalName = "") && (..ModifiedFiles(i).changeType '= "D")) { write !, ..ModifiedFiles(i).externalName, " was not imported into the database and will not be compiled. " } elseif (..ModifiedFiles(i).changeType = "D") { - set sc = ..DeleteFile(internalName) + set sc = ..DeleteFile(internalName, ..ModifiedFiles(i).externalName) if sc { write !, ..ModifiedFiles(i).externalName, " was deleted." } else { @@ -38,10 +38,13 @@ Method OnPull() As %Status quit $system.OBJ.CompileList(.compilelist, "ck") } -Method DeleteFile(item As %String) As %Status +Method DeleteFile(item As %String = "", externalName As %String = "") As %Status { set sc = $$$OK - set type = ##class(SourceControl.Git.Utils).Type(item) + set type = $select( + ..ItemIsPTD(externalName): "ptd", + 1: ##class(SourceControl.Git.Utils).Type(item) + ) set name = ##class(SourceControl.Git.Utils).NameWithoutExtension(item) set settings = ##class(SourceControl.Git.Settings).%New() set deleted = 1 @@ -54,8 +57,9 @@ Method DeleteFile(item As %String) As %Status }elseif type = "csp" { set sc = $System.CSP.DeletePage(item) } elseif settings.decomposeProductions && (type = "ptd") { + set normalizedFilePath = ##class(%File).NormalizeFilename(##class(SourceControl.Git.Utils).TempFolder()_externalName) set sc = ##class(%SYSTEM.Status).AppendStatus( - ##class(SourceControl.Git.Production).RemoveItem(item), + ##class(SourceControl.Git.Production).RemoveItemByExternalName(normalizedFilePath,"FullExternalName"), ##class(%Library.RoutineMgr).Delete(item) ) }elseif ##class(SourceControl.Git.Utils).UserTypeCached(item) { @@ -65,8 +69,10 @@ Method DeleteFile(item As %String) As %Status } if deleted && $$$ISOK(sc) { - do ##class(SourceControl.Git.Utils).RemoveRoutineTSH(item) - kill $$$TrackedItems(##class(SourceControl.Git.Utils).NormalizeExtension(item)) + if (item '= "") { + do ##class(SourceControl.Git.Utils).RemoveRoutineTSH(item) + kill $$$TrackedItems(##class(SourceControl.Git.Utils).NormalizeExtension(item)) + } } else { if +$system.Status.GetErrorCodes(sc) = $$$ClassDoesNotExist { // if something we wanted to delete is already deleted -- good! @@ -76,4 +82,23 @@ Method DeleteFile(item As %String) As %Status return sc } +/// Determines whether an item has type PTD based on the external name, not reliant on the file existing +ClassMethod ItemIsPTD(externalName) As %Boolean +{ + if $zconvert($piece(externalName,".",*),"l") '= "xml" { + return 0 + } + set settings = ##class(SourceControl.Git.Settings).%New() + set normFilePath = $replace(externalName,"\","/") + set key = $order($$$SourceMapping("PTD","")) + while (key '= "") { + set directory = $replace($$$SourceMapping("PTD",key), "\","/") + if $find(normFilePath, directory) = ($length(directory) + 1) { + return 1 + } + set key = $order($$$SourceMapping("PTD",key)) + } + return 0 +} + } diff --git a/test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls new file mode 100644 index 00000000..6c7a2e26 --- /dev/null +++ b/test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -0,0 +1,34 @@ +Include SourceControl.Git + +Class UnitTest.SourceControl.Git.PullEventHandler.IncrementalLoad Extends %UnitTest.TestCase +{ + +Property Mappings [ MultiDimensional ]; + +Method TestItemIsPTD() +{ + do $$$AssertNotTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("cls/test.xml")) + do $$$AssertNotTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd/test.md")) + do $$$AssertNotTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("")) + do $$$AssertTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd/test.xml")) + do $$$AssertTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd2/test.xml")) + do $$$AssertTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd2\test.xml")) +} + +Method OnBeforeAllTests() As %Status +{ + merge ..Mappings = @##class(SourceControl.Git.Utils).MappingsNode() + kill @##class(SourceControl.Git.Utils).MappingsNode() + set $$$SourceMapping("PTD", "*") = "ptd/" + set $$$SourceMapping("PTD", "Some.Production") = "ptd2/" + quit $$$OK +} + +Method %OnClose() As %Status +{ + kill @##class(SourceControl.Git.Utils).MappingsNode() + merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings + quit $$$OK +} + +} From 4605a98de56f835bbee0fb3efd85537d61cf67af Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:59:19 -0400 Subject: [PATCH 49/79] fix: stale ptd entries in items cache no longer breaks Export All --- cls/SourceControl/Git/Production.cls | 7 +++++-- cls/SourceControl/Git/Utils.cls | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 1e359edf..6ab50384 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -87,12 +87,15 @@ ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status Set name = $Piece(internalName,".",1,$Length(internalName,".")-1) Set $ListBuild(productionName, itemName) = $ListFromString(name, "||") Set $ListBuild(itemName, itemClassName) = $ListFromString(itemName, "|") + Set sc = $$$OK If $Piece($Piece(name,"||",2),"|",2) = "" { Set sc = ..ExportProductionSettings(productionName, nameMethod) } Else { Set configItemName = productionName_"||"_$Piece(itemName, "Settings-", 2)_"|"_itemClassName Set item = ##class(Ens.Config.Production).OpenItemByConfigName(configItemName) - Set sc = ..ExportConfigItemSettings(productionName, item, nameMethod) + If $IsObject(item) { + Set sc = ..ExportConfigItemSettings(productionName, item, nameMethod) + } } Return sc } @@ -115,7 +118,7 @@ ClassMethod ExportConfigItemSettings(productionClass As %String, item As %Regist /// class name ClassMethod ExportProductionSettings(productionClass As %String, nameMethod As %String) As %Status { - set internalName = ..CreateInternalName(productionClass,,,0) + set internalName = ..CreateInternalName(productionClass,,,1) Set class = ##class(%Dictionary.CompiledClass).%OpenId(productionClass) Set sc = ##class(Ens.Deployment.Utils).CreatePTDFromProduction(class, .ptdName) If $$$ISERR(sc) { diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 2dbad1a3..60a69c42 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1550,7 +1550,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As } else { $$$QuitOnError($system.OBJ.ExportUDL(InternalName, filename,"-d/diff")) } - if (filename '= "") { + if (filename '= "") && ##class(%File).Exists(filename) { set filenames($I(filenames)) = filename $$$QuitOnError(..UpdateRoutineTSH(InternalName, $h)) if '##class(SourceControl.Git.Change).IsUncommitted(filename) { From 0528427e2183105522543198c04cb2fafde19029 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:30:16 -0400 Subject: [PATCH 50/79] added production baseline to be run after enabling or disabling production decomposition --- cls/SourceControl/Git/API.cls | 6 ++ cls/SourceControl/Git/Production.cls | 15 ++-- .../Git/PullEventHandler/IncrementalLoad.cls | 27 ++------ cls/SourceControl/Git/Util/Production.cls | 69 +++++++++++++++++++ .../Git/PullEventHandler/IncrementalLoad.cls | 34 --------- .../SourceControl/Git/Util/Production.cls | 34 +++++++++ 6 files changed, 123 insertions(+), 62 deletions(-) create mode 100644 cls/SourceControl/Git/Util/Production.cls delete mode 100644 test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls create mode 100644 test/UnitTest/SourceControl/Git/Util/Production.cls diff --git a/cls/SourceControl/Git/API.cls b/cls/SourceControl/Git/API.cls index 0e10964e..33f66368 100644 --- a/cls/SourceControl/Git/API.cls +++ b/cls/SourceControl/Git/API.cls @@ -64,5 +64,11 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status quit ##class(SourceControl.Git.Utils).BaselineExport(pCommitMessage, pPushToRemote) } +/// Run to baseline all interoperability productions in the namespace to source control. +/// This should be done after changing the value of the "decompose productions" setting. +ClassMethod BaselineProductions() +{ + do ##class(SourceControl.Git.Util.Production).BaselineProductions() } +} diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 6ab50384..6dadfc5a 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -6,14 +6,14 @@ Class SourceControl.Git.Production Extends %RegisteredObject /// Exports settings for a given Production and each Config Item from /// the ProductionDefinition as separate XMLs. These are exported to -/// the /ptd subdirectory under the client's ^Sources directory. -ClassMethod ExportProductionDefinitionShards(productionClass As %String, nameMethod As %String) As %Status +/// the appropriate file based on nameMethod of the source control class +ClassMethod ExportProductionDefinitionShards(productionClass As %String, nameMethod As %String, Output internalNames) As %Status { - // First, export Production definition omitting Config Items - Set sc = ..ExportProductionSettings(productionClass, nameMethod) + Set sc = ..ExportProductionSettings(productionClass, nameMethod, .internalName) If $$$ISERR(sc) { Return sc } + Set internalNames(internalName) = 1 // next, export each item to a separate file Set rs = ##class(%SQL.Statement).%ExecDirect(, @@ -25,10 +25,11 @@ ClassMethod ExportProductionDefinitionShards(productionClass As %String, nameMet Set ptdName = "" Set item = ##class(Ens.Config.Production).OpenItemByConfigName(productionClass _ "||" _ rs.Name _ "|" _ rs.ClassName) If $isobject(item) { - Set sc = ..ExportConfigItemSettings(productionClass, item, nameMethod) + Set sc = ..ExportConfigItemSettings(productionClass, item, nameMethod, .internalName) If $$$ISERR(sc) { Return sc } + Set internalNames(internalName) = 1 } } Return $$$OK @@ -102,7 +103,7 @@ ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status /// Export a single Production Config Item. For a given Ens.Config.Item, the /// exports the PTD for this item to the file system under the directory specified -ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String) As %Status +ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String, Output internalName As %String) As %Status { set internalName = ..CreateInternalName(productionClass, item.Name, item.ClassName, 0) Set externalName = $ClassMethod($$SrcCtrlCls^%buildccr, nameMethod, internalName) @@ -116,7 +117,7 @@ ClassMethod ExportConfigItemSettings(productionClass As %String, item As %Regist /// Exports the Production settings from ProductionDefinition given the Production /// class name -ClassMethod ExportProductionSettings(productionClass As %String, nameMethod As %String) As %Status +ClassMethod ExportProductionSettings(productionClass As %String, nameMethod As %String, Output internalName As %String) As %Status { set internalName = ..CreateInternalName(productionClass,,,1) Set class = ##class(%Dictionary.CompiledClass).%OpenId(productionClass) diff --git a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls index 0b9ac56e..b393aa75 100644 --- a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -42,7 +42,7 @@ Method DeleteFile(item As %String = "", externalName As %String = "") As %Status { set sc = $$$OK set type = $select( - ..ItemIsPTD(externalName): "ptd", + ##class(SourceControl.Git.Util.Production).ItemIsPTD(externalName): "ptd", 1: ##class(SourceControl.Git.Utils).Type(item) ) set name = ##class(SourceControl.Git.Utils).NameWithoutExtension(item) @@ -51,7 +51,11 @@ Method DeleteFile(item As %String = "", externalName As %String = "") As %Status if type = "prj" { set sc = $system.OBJ.DeleteProject(name) }elseif type = "cls" { - set sc = $system.OBJ.Delete(item) + if settings.decomposeProductions && ##class(SourceControl.Git.Production).IsProductionClass(name, "FullExternalName") { + write !, "Production decomposition enabled, skipping delete of production class" + } else { + set sc = $system.OBJ.Delete(item) + } }elseif $listfind($listbuild("mac","int","inc","bas","mvb","mvi"), type) > 0 { set sc = ##class(%Routine).Delete(item) }elseif type = "csp" { @@ -82,23 +86,4 @@ Method DeleteFile(item As %String = "", externalName As %String = "") As %Status return sc } -/// Determines whether an item has type PTD based on the external name, not reliant on the file existing -ClassMethod ItemIsPTD(externalName) As %Boolean -{ - if $zconvert($piece(externalName,".",*),"l") '= "xml" { - return 0 - } - set settings = ##class(SourceControl.Git.Settings).%New() - set normFilePath = $replace(externalName,"\","/") - set key = $order($$$SourceMapping("PTD","")) - while (key '= "") { - set directory = $replace($$$SourceMapping("PTD",key), "\","/") - if $find(normFilePath, directory) = ($length(directory) + 1) { - return 1 - } - set key = $order($$$SourceMapping("PTD",key)) - } - return 0 -} - } diff --git a/cls/SourceControl/Git/Util/Production.cls b/cls/SourceControl/Git/Util/Production.cls new file mode 100644 index 00000000..a6692601 --- /dev/null +++ b/cls/SourceControl/Git/Util/Production.cls @@ -0,0 +1,69 @@ +Include SourceControl.Git + +/// Contains utilities for production decomposition that are specific to git-source-control +Class SourceControl.Git.Util.Production +{ + +/// Baselines all productions in this namespace from single-file to decomposed or vice versa. +ClassMethod BaselineProductions() +{ + set st = $$$OK + set settings = ##class(SourceControl.Git.Settings).%New() + set rs = ##class(%Dictionary.ClassDefinition).SubclassOfFunc("Ens.Production") + throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) + while rs.%Next(.sc) { + $$$ThrowOnError(sc) + set productionName = rs.Name + set productionInternalName = productionName _ ".cls" + if '##class(SourceControl.Git.Utils).FileIsMapped(productionInternalName) { + if settings.decomposeProductions { + write !, "Decomposing production: " _ productionInternalName + if ##class(SourceControl.Git.Utils).IsInSourceControl(productionInternalName) { + set st = ##class(SourceControl.Git.Utils).RemoveFromSourceControl(productionInternalName) + $$$ThrowOnError(st) + } + set st = ##class(SourceControl.Git.Production).ExportProductionDefinitionShards(productionName,"FullExternalName",.itemInternalNames) + $$$ThrowOnError(st) + set key = $order(itemInternalNames("")) + while (key '= "") { + set st = ##class(SourceControl.Git.Utils).AddToSourceControl(key) + $$$ThrowOnError(st) + set key = $order(itemInternalNames(key)) + } + } else { + write !, "Recomposing production: " _ productionInternalName + set st = ##class(SourceControl.Git.Utils).AddToSourceControl(productionInternalName) + $$$ThrowOnError(st) + set key = $order(@##class(SourceControl.Git.Utils).#Storage@("items", "")) + while (key '= "") { + if $match(key,"^"_productionName_"\|\|.*\.(?i)ptd$") { + set st = ##class(SourceControl.Git.Utils).RemoveFromSourceControl(key) + $$$ThrowOnError(st) + } + set key = $order(@##class(SourceControl.Git.Utils).#Storage@("items", key)) + } + } + } + } +} + +/// Determines whether an item has type PTD based on the external name, not reliant on the file existing +ClassMethod ItemIsPTD(externalName) As %Boolean +{ + if $zconvert($piece(externalName,".",*),"l") '= "xml" { + return 0 + } + set settings = ##class(SourceControl.Git.Settings).%New() + set normFilePath = $replace(externalName,"\","/") + set key = $order($$$SourceMapping("PTD","")) + while (key '= "") { + set directory = $replace($$$SourceMapping("PTD",key), "\","/") + if $find(normFilePath, directory) = ($length(directory) + 1) { + return 1 + } + set key = $order($$$SourceMapping("PTD",key)) + } + return 0 +} + +} diff --git a/test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls deleted file mode 100644 index 6c7a2e26..00000000 --- a/test/UnitTest/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ /dev/null @@ -1,34 +0,0 @@ -Include SourceControl.Git - -Class UnitTest.SourceControl.Git.PullEventHandler.IncrementalLoad Extends %UnitTest.TestCase -{ - -Property Mappings [ MultiDimensional ]; - -Method TestItemIsPTD() -{ - do $$$AssertNotTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("cls/test.xml")) - do $$$AssertNotTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd/test.md")) - do $$$AssertNotTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("")) - do $$$AssertTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd/test.xml")) - do $$$AssertTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd2/test.xml")) - do $$$AssertTrue(##class(SourceControl.Git.PullEventHandler.IncrementalLoad).ItemIsPTD("ptd2\test.xml")) -} - -Method OnBeforeAllTests() As %Status -{ - merge ..Mappings = @##class(SourceControl.Git.Utils).MappingsNode() - kill @##class(SourceControl.Git.Utils).MappingsNode() - set $$$SourceMapping("PTD", "*") = "ptd/" - set $$$SourceMapping("PTD", "Some.Production") = "ptd2/" - quit $$$OK -} - -Method %OnClose() As %Status -{ - kill @##class(SourceControl.Git.Utils).MappingsNode() - merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings - quit $$$OK -} - -} diff --git a/test/UnitTest/SourceControl/Git/Util/Production.cls b/test/UnitTest/SourceControl/Git/Util/Production.cls new file mode 100644 index 00000000..2ccbf11f --- /dev/null +++ b/test/UnitTest/SourceControl/Git/Util/Production.cls @@ -0,0 +1,34 @@ +Include SourceControl.Git + +Class UnitTest.SourceControl.Git.Util.Production Extends %UnitTest.TestCase +{ + +Property Mappings [ MultiDimensional ]; + +Method TestItemIsPTD() +{ + do $$$AssertNotTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("cls/test.xml")) + do $$$AssertNotTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd/test.md")) + do $$$AssertNotTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("")) + do $$$AssertTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd/test.xml")) + do $$$AssertTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd2/test.xml")) + do $$$AssertTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd2\test.xml")) +} + +Method OnBeforeAllTests() As %Status +{ + merge ..Mappings = @##class(SourceControl.Git.Utils).MappingsNode() + kill @##class(SourceControl.Git.Utils).MappingsNode() + set $$$SourceMapping("PTD", "*") = "ptd/" + set $$$SourceMapping("PTD", "Some.Production") = "ptd2/" + quit $$$OK +} + +Method %OnClose() As %Status +{ + kill @##class(SourceControl.Git.Utils).MappingsNode() + merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings + quit $$$OK +} + +} From e3966756f3063c0d7041e62ca2b41df9c14eb7a3 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:53:43 -0400 Subject: [PATCH 51/79] fix: items cache now populated correctly when adding new production --- cls/SourceControl/Git/File.cls | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cls/SourceControl/Git/File.cls b/cls/SourceControl/Git/File.cls index 3432fccf..9ace3374 100644 --- a/cls/SourceControl/Git/File.cls +++ b/cls/SourceControl/Git/File.cls @@ -47,7 +47,7 @@ ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String } } if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() { - do ##class(SourceControl.Git.Production).ParseExternalName(ExternalName,.internalName) + do ##class(SourceControl.Git.Production).ParseExternalName($replace(ExternalName,"\","/"),.internalName) } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") { set internalName = outName } @@ -84,4 +84,3 @@ Storage Default } } - From f0652de7897d15b683dec5e29598566d73b03e89 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 6 Sep 2024 13:54:07 -0400 Subject: [PATCH 52/79] added tests for production decomposition --- .../Git/ProductionDecomposition.cls | 169 ++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 test/UnitTest/SourceControl/Git/ProductionDecomposition.cls diff --git a/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls b/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls new file mode 100644 index 00000000..71c86ed2 --- /dev/null +++ b/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls @@ -0,0 +1,169 @@ +Include SourceControl.Git + +Class UnitTest.SourceControl.Git.ProductionDecomposition Extends %UnitTest.TestCase +{ + +Parameter ProductionName = "UnitTest.SampleProduction"; + +Property SourceControlGlobal [ MultiDimensional ]; + +Property InitialExtension As %String [ InitialExpression = {##class(%Studio.SourceControl.Interface).SourceControlClassGet()} ]; + +Method TestDecomposeExistingProduction() +{ + do $System.OBJ.Delete(..#ProductionName) + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.decomposeProductions = 0 + $$$ThrowOnError(settings.%Save()) + $$$ThrowOnError(##class(SourceControl.Git.Production).CreateProduction(..#ProductionName)) + do ..ReplaceProductionDefinition("ProductionDefinition1") + $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(..#ProductionName_".cls")) + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(..#ProductionName_".cls")) + $$$ThrowOnError(##class(SourceControl.Git.Utils).Commit(..#ProductionName_".cls")) + + set settings.decomposeProductions = 1 + $$$ThrowOnError(settings.%Save()) + do ##class(SourceControl.Git.API).BaselineProductions() + do ##class(SourceControl.Git.Utils).RunGitCommand("add",,,".") + do ##class(SourceControl.Git.Utils).RunGitCommand("commit",,,"-m","decomposing production from unit test") + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||ProductionSettings-UnitTest.SampleProduction.PTD")) + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||Settings-a|Ens.Activity.Operation.Local.PTD")) + do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(..#ProductionName_".cls")) + + set settings.decomposeProductions = 0 + $$$ThrowOnError(settings.%Save()) + do ##class(SourceControl.Git.API).BaselineProductions() + do ##class(SourceControl.Git.Utils).RunGitCommand("add",,,".") + do ##class(SourceControl.Git.Utils).RunGitCommand("commit",,,"-m","recomposing production from unit test") + do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||ProductionSettings-UnitTest.SampleProduction.PTD")) + do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||Settings-a|Ens.Activity.Operation.Local.PTD")) + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl(..#ProductionName_".cls")) +} + +Method TestEditProduction() +{ + new %session, %request, %SourceControl + set %session = ##class(%CSP.Session).%New("dummysession") + set %request = ##class(%CSP.Request).%New() + set %request.Data("pageclass",1) = "EnsPortal.dummy" + do $System.OBJ.Delete(..#ProductionName) + $$$ThrowOnError(##class(SourceControl.Git.Utils).NewBranch("branch1")) + $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("branch1")) + $$$ThrowOnError(##class(SourceControl.Git.Production).CreateProduction(..#ProductionName)) + do ##class(%Studio.SourceControl.Interface).SourceControlCreate() + $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(..#ProductionName_".cls")) + + do $$$LogMessage("with production decomposition enabled, the production class should not be in source control.") + do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl(..#ProductionName_".cls")) + do $$$LogMessage("initial creation of a production should export production settings and add to source control.") + $$$ThrowOnError(%SourceControl.OnBeforeSave(..#ProductionName_".cls")) + $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||ProductionSettings-UnitTest.SampleProduction.PTD")) + + do $$$LogMessage("adding a production item") + $$$ThrowOnError(%SourceControl.OnBeforeSave(..#ProductionName_".cls")) + do ..ReplaceProductionDefinition("ProductionDefinition1") + $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||Settings-a|Ens.Activity.Operation.Local.PTD")) + + do $$$LogMessage("committing changes to production settings") + do ##class(SourceControl.Git.Utils).RunGitCommand("add",,,".") + do ##class(SourceControl.Git.Utils).Commit("UnitTest.SampleProduction||ProductionSettings-UnitTest.SampleProduction.PTD") + do ##class(SourceControl.Git.Utils).Commit("UnitTest.SampleProduction||Settings-a|Ens.Activity.Operation.Local.PTD") + + do $$$LogMessage("switching to a new branch") + $$$ThrowOnError(##class(SourceControl.Git.Utils).NewBranch("branch2")) + $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("branch2")) + + do $$$LogMessage("modifying an existing item and adding a new item") + $$$ThrowOnError(%SourceControl.OnBeforeSave(..#ProductionName_".cls")) + set production = ##class(Ens.Config.Production).%OpenId(..#ProductionName) + set production.Items.GetAt(1).Enabled = 0 + do ..ReplaceProductionDefinition("ProductionDefinition2") + $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||Settings-b|Ens.Activity.Operation.Local.PTD")) + do ##class(SourceControl.Git.Utils).RunGitCommand("add",,,".") + do ##class(SourceControl.Git.Utils).Commit("UnitTest.SampleProduction||Settings-a|Ens.Activity.Operation.Local.PTD") + do ##class(SourceControl.Git.Utils).Commit("UnitTest.SampleProduction||Settings-b|Ens.Activity.Operation.Local.PTD") + $$$ThrowOnError(production.%Reload()) + do $$$AssertEquals(production.Items.Count(), 2) + + do $$$LogMessage("switching back to the original branch") + $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("branch1")) + $$$ThrowOnError(production.%Reload()) + do $$$AssertEquals(production.Items.Count(), 1) + do $$$AssertEquals(production.Items.GetAt(1).Settings.GetAt(1).Name, "RecordStatsInterval") + do $$$AssertEquals(production.Items.GetAt(1).Settings.GetAt(1).Value, 61) +} + +ClassMethod ReplaceProductionDefinition(pXDataName) +{ + new %SourceControl + set productionClass = ##class(%Dictionary.ClassDefinition).%OpenId(..#ProductionName) + do productionClass.XDatas.Clear() + set productionXData = ##class(%Dictionary.XDataDefinition).%New() + set xdata = ##class(%Dictionary.XDataDefinition).IDKEYOpen($classname(),pXDataName,,.st) + $$$ThrowOnError(st) + set productionXData = xdata.%ConstructClone(1) + set productionXData.Name = "ProductionDefinition" + set st = productionClass.XDatas.Insert(productionXData) + $$$ThrowOnError(st) + set st = productionClass.%Save() + $$$ThrowOnError(st) + set st = $System.OBJ.Compile(..#ProductionName) + $$$ThrowOnError(st) + set st = ##class(Ens.Production).Update() + $$$ThrowOnError(st) +} + +XData ProductionDefinition1 +{ + + + + +} + +XData ProductionDefinition2 +{ + + + 61 + + + + +} + +Method OnBeforeAllTests() As %Status +{ + merge ..SourceControlGlobal = ^SYS("SourceControl") + return $$$OK +} + +Method OnBeforeOneTest() As %Status +{ + kill ^SYS("SourceControl") + do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension") + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.decomposeProductions = 1 + Set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"dir" + Set settings.Mappings("PTD","*")="ptd/" + $$$ThrowOnError(settings.%Save()) + // using work queue manager ensures proper OS user context/file ownership + set workMgr = $System.WorkMgr.%New("") + $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) + $$$ThrowOnError(workMgr.WaitForComplete()) + quit $$$OK +} + +Method %OnClose() As %Status +{ + do ##class(%Studio.SourceControl.Interface).SourceControlClassSet(..InitialExtension) + kill ^SYS("SourceControl") + merge ^SYS("SourceControl") = ..SourceControlGlobal + do $System.OBJ.Delete(..#ProductionName) + quit $$$OK +} + +} From 15a2760116d4d29bfeeb8194bed931f636202a5c Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:09:18 -0400 Subject: [PATCH 53/79] production decomposition refactoring and code style improvements --- cls/SourceControl/Git/Production.cls | 26 +++++++++---------- .../Git/PullEventHandler/IncrementalLoad.cls | 2 +- cls/SourceControl/Git/Utils.cls | 4 +-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 6dadfc5a..3fbd7dbb 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -106,7 +106,7 @@ ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String, Output internalName As %String) As %Status { set internalName = ..CreateInternalName(productionClass, item.Name, item.ClassName, 0) - Set externalName = $ClassMethod($$SrcCtrlCls^%buildccr, nameMethod, internalName) + Set externalName = $ClassMethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, internalName) Set filename = ##class(%File).NormalizeFilename(externalName) set st = ##class(Ens.Deployment.Utils).CreatePTDFromItem(.item, .ptdName) $$$QuitOnError(st) @@ -125,7 +125,7 @@ ClassMethod ExportProductionSettings(productionClass As %String, nameMethod As % If $$$ISERR(sc) { Return sc } - Set externalName = $ClassMethod($$SrcCtrlCls^%buildccr, nameMethod, internalName) + Set externalName = $ClassMethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, internalName) Set filename = ##class(%File).NormalizeFilename(externalName) set sc = ..ExportProjectForPTD(productionClass, ptdName, filename) Return sc @@ -172,16 +172,16 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt // FUTURE: implement this to support Studio } // populate data for use in OnAfterSave - kill ^mtempsscProd($job,"modifiedItems") - merge ^mtempsscProd($job,"modifiedItems") = modifiedItems + kill ^IRIS.Temp("sscProd",$job,"modifiedItems") + merge ^IRIS.Temp("sscProd",$job,"modifiedItems") = modifiedItems // FUTURE: use a percent variable or PPG instead - kill ^mtempsscProd($job,"items") + kill ^IRIS.Temp("sscProd",$job,"items") set rs = ##class(%SQL.Statement).%ExecDirect( ,"select Name, ClassName from Ens_Config.Item where Production = ?" , productionName) $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) while rs.%Next() { - set ^mtempsscProd($job,"items",$listbuild(rs.Name, rs.ClassName)) = 1 + set ^IRIS.Temp("sscProd",$job,"items",$listbuild(rs.Name, rs.ClassName)) = 1 } } @@ -196,22 +196,22 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) , productionName) $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) while rs.%Next() { - if '$get(^mtempsscProd($job,"items", $listbuild(rs.Name, rs.ClassName))) { + if '$get(^IRIS.Temp("sscProd",$job,"items", $listbuild(rs.Name, rs.ClassName))) { set itemInternalName = ..CreateInternalName(productionName, rs.Name, rs.ClassName, 0) set modifiedItems(itemInternalName) = "A" } - kill ^mtempsscProd($job,"items", $listbuild(rs.Name, rs.ClassName)) + kill ^IRIS.Temp("sscProd",$job,"items", $listbuild(rs.Name, rs.ClassName)) } - set key = $order(^mtempsscProd($job,"items","")) + set key = $order(^IRIS.Temp("sscProd",$job,"items","")) while (key '= "") { set itemInternalName = ..CreateInternalName(productionName, $listget(key,1), $listget(key,2), 0) set modifiedItems(itemInternalName) = "D" - set key = $order(^mtempsscProd($job,"items",key)) + set key = $order(^IRIS.Temp("sscProd",$job,"items",key)) } // If editing from SMP, get the modified items from a cache stored in OnBeforeSave. // Only do this if there are no added/deleted items, because otherwise production settings will be incorrectly included. if '$data(modifiedItems) { - merge modifiedItems = ^mtempsscProd($job,"modifiedItems") + merge modifiedItems = ^IRIS.Temp("sscProd",$job,"modifiedItems") } } else { // If editing/adding/deleting from Studio, get the modified items from a percent variable set in OnBeforeSave. @@ -234,7 +234,7 @@ ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %B if (className '= "") && $$$comClassDefined(className) { return $classmethod(className, "%Extends", "Ens.Production") } else { - set filename = $classmethod($$SrcCtrlCls^%buildccr, nameMethod, className_".CLS") + set filename = $classmethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, className_".CLS") if ##class(%File).Exists(filename) { $$$ThrowOnError($System.OBJ.Load(filename)) } @@ -314,7 +314,7 @@ ClassMethod RemoveItemByExternalName(externalName, nameMethod) As %Status for i=1:1:production.Items.Count() { set configItem = production.Items.GetAt(i) set itemInternalName = ..CreateInternalName(productionName, configItem.Name, configItem.ClassName) - set itemExternalName = $classmethod($$SrcCtrlCls^%buildccr, nameMethod, itemInternalName) + set itemExternalName = $classmethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, itemInternalName) if itemExternalName = externalName { set itemToRemove = configItem quit diff --git a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls index b393aa75..861253af 100644 --- a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -51,7 +51,7 @@ Method DeleteFile(item As %String = "", externalName As %String = "") As %Status if type = "prj" { set sc = $system.OBJ.DeleteProject(name) }elseif type = "cls" { - if settings.decomposeProductions && ##class(SourceControl.Git.Production).IsProductionClass(name, "FullExternalName") { + if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(name) { write !, "Production decomposition enabled, skipping delete of production class" } else { set sc = $system.OBJ.Delete(item) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 60a69c42..5c7032bb 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1280,9 +1280,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A do ##class(%File).Delete(rollbackFile) } } - } elseif (type = "cls") && settings.decomposeProductions - && ##class(SourceControl.Git.Production).IsProductionClass( - ..NameWithoutExtension(InternalName), "FullExternalName") { + } elseif ..ItemIsProductionToDecompose(InternalName) { write !, "Production decomposition enabled, skipping import of production class" set imported = 0 } elseif ..UserTypeCached(InternalName,.docclass,.doctype) { From a86e8b8cd8d2597c1429a49c6982cfeed31920b7 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Mon, 9 Sep 2024 15:11:16 -0400 Subject: [PATCH 54/79] settings UI for decompose productions flag --- csp/gitprojectsettings.csp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/csp/gitprojectsettings.csp b/csp/gitprojectsettings.csp index 3043afd4..666cd9be 100644 --- a/csp/gitprojectsettings.csp +++ b/csp/gitprojectsettings.csp @@ -91,6 +91,7 @@ body { } set settings.compileOnImport = ($Get(%request.Data("compileOnImport", 1)) = 1) + set settings.decomposeProductions = ($Get(%request.Data("decomposeProductions", 1)) = 1) if ($Get(%request.Data("basicMode", 1)) = 1) { set settings.basicMode = 1 @@ -348,6 +349,18 @@ body { +
+ +
+
+ + +
+
+
From dab1a6f28f45a9089614c36806f5acc040a5af72 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:07:37 -0400 Subject: [PATCH 55/79] fix bug getting modified production item settings --- cls/SourceControl/Git/Production.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 3fbd7dbb..0f026872 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -148,7 +148,7 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt quit } for j=1:1:item.Settings.Count() { - set setting = item.Settings.GetAt(i) + set setting = item.Settings.GetAt(j) if $isobject(setting) && setting.%IsModified() { set modifiedItem = item quit From 5778f7f8222652cd0d8c2d4d10b17019aeefaf77 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 10 Sep 2024 11:25:39 -0400 Subject: [PATCH 56/79] production decomp unit test correctly tests deployment --- .../Git/ProductionDecomposition.cls | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls b/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls index 71c86ed2..3ca53fe2 100644 --- a/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls +++ b/test/UnitTest/SourceControl/Git/ProductionDecomposition.cls @@ -48,7 +48,6 @@ Method TestEditProduction() set %request.Data("pageclass",1) = "EnsPortal.dummy" do $System.OBJ.Delete(..#ProductionName) $$$ThrowOnError(##class(SourceControl.Git.Utils).NewBranch("branch1")) - $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("branch1")) $$$ThrowOnError(##class(SourceControl.Git.Production).CreateProduction(..#ProductionName)) do ##class(%Studio.SourceControl.Interface).SourceControlCreate() $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl(..#ProductionName_".cls")) @@ -60,7 +59,7 @@ Method TestEditProduction() $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||ProductionSettings-UnitTest.SampleProduction.PTD")) - do $$$LogMessage("adding a production item") + do $$$LogMessage("adding a production item should add it to source control") $$$ThrowOnError(%SourceControl.OnBeforeSave(..#ProductionName_".cls")) do ..ReplaceProductionDefinition("ProductionDefinition1") $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) @@ -73,22 +72,26 @@ Method TestEditProduction() do $$$LogMessage("switching to a new branch") $$$ThrowOnError(##class(SourceControl.Git.Utils).NewBranch("branch2")) - $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("branch2")) - do $$$LogMessage("modifying an existing item and adding a new item") + do $$$LogMessage("adding a new item and modifying an existing item") $$$ThrowOnError(%SourceControl.OnBeforeSave(..#ProductionName_".cls")) - set production = ##class(Ens.Config.Production).%OpenId(..#ProductionName) - set production.Items.GetAt(1).Enabled = 0 do ..ReplaceProductionDefinition("ProductionDefinition2") $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) + set production = ##class(Ens.Config.Production).%OpenId(..#ProductionName) + set production.Items.GetAt(1).Settings.GetAt(1).Value = 71 + $$$ThrowOnError(%SourceControl.OnBeforeSave(..#ProductionName_".cls")) + do ..ReplaceProductionDefinition("ProductionDefinition3") + $$$ThrowOnError(%SourceControl.OnAfterSave(..#ProductionName_".cls")) do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("UnitTest.SampleProduction||Settings-b|Ens.Activity.Operation.Local.PTD")) do ##class(SourceControl.Git.Utils).RunGitCommand("add",,,".") do ##class(SourceControl.Git.Utils).Commit("UnitTest.SampleProduction||Settings-a|Ens.Activity.Operation.Local.PTD") do ##class(SourceControl.Git.Utils).Commit("UnitTest.SampleProduction||Settings-b|Ens.Activity.Operation.Local.PTD") $$$ThrowOnError(production.%Reload()) do $$$AssertEquals(production.Items.Count(), 2) + do $$$AssertEquals(production.Items.GetAt(1).Settings.GetAt(1).Name, "RecordStatsInterval") + do $$$AssertEquals(production.Items.GetAt(1).Settings.GetAt(1).Value, 71) - do $$$LogMessage("switching back to the original branch") + do $$$LogMessage("switching back to the original branch should modify and delete items") $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("branch1")) $$$ThrowOnError(production.%Reload()) do $$$AssertEquals(production.Items.Count(), 1) @@ -120,10 +123,12 @@ XData ProductionDefinition1 { + 61 } +/// adds item b XData ProductionDefinition2 { @@ -135,6 +140,18 @@ XData ProductionDefinition2 } +/// modifies a setting for item a +XData ProductionDefinition3 +{ + + + 71 + + + + +} + Method OnBeforeAllTests() As %Status { merge ..SourceControlGlobal = ^SYS("SourceControl") From 43c4b71f4c4c2ab848d85bf95359c0575755fd37 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 17 Sep 2024 15:15:23 -0400 Subject: [PATCH 57/79] git-source-control settings are now a source-controlled file --- .../Git/PackageManagerContext.cls | 5 +- cls/SourceControl/Git/Settings.cls | 66 +++++++++- cls/SourceControl/Git/Settings/Document.cls | 108 ++++++++++++++++ cls/SourceControl/Git/Utils.cls | 7 ++ csp/gitprojectsettings.csp | 21 +++- test/UnitTest/SourceControl/Git/Settings.cls | 119 ++++++++++++++++++ 6 files changed, 319 insertions(+), 7 deletions(-) create mode 100644 cls/SourceControl/Git/Settings/Document.cls create mode 100644 test/UnitTest/SourceControl/Git/Settings.cls diff --git a/cls/SourceControl/Git/PackageManagerContext.cls b/cls/SourceControl/Git/PackageManagerContext.cls index 874bbad0..103f4722 100644 --- a/cls/SourceControl/Git/PackageManagerContext.cls +++ b/cls/SourceControl/Git/PackageManagerContext.cls @@ -18,6 +18,10 @@ Method InternalNameSet(InternalName As %String = "") As %Status set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName) if (InternalName '= i%InternalName) { set i%InternalName = InternalName + if (InternalName = ##class(SourceControl.Git.Settings.Document).#INTERNALNAME) { + // git source control settings document is never in an IPM context + quit $$$OK + } if $$$comClassDefined("%IPM.ExtensionBase.Utils") { set ..Package = ##class(%IPM.ExtensionBase.Utils).FindHomeModule(InternalName,,.resourceReference) } elseif $$$comClassDefined("%ZPM.PackageManager.Developer.Extension.Utils") { @@ -51,4 +55,3 @@ Method Dump() } } - diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index e7c03c6b..9f30a368 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -66,6 +66,31 @@ Method %OnNew() As %Status quit $$$OK } +Method SaveWithSourceControl() As %Status +{ + set sc = $$$OK + #dim %SourceControl As %Studio.SourceControl.Interface + if '$isobject($get(%SourceControl)) { + do ##class(%Studio.SourceControl.Interface).SourceControlCreate($username) + } + // Source control settings naively by only calling OnAfterSave hooks + // Future enhancement: add full source control support with settings UI + set internalName = ##class(SourceControl.Git.Settings.Document).#INTERNALNAME + set settingsDoc = ##class(SourceControl.Git.Settings.Document).%New(internalName) + if (##class(%Studio.SourceControl.Interface).SourceControlClassGet() = ##class(SourceControl.Git.Extension).%ClassName(1)) { + if '##class(SourceControl.Git.Utils).IsInSourceControl(internalName) { + set sc = ##class(SourceControl.Git.Utils).AddToSourceControl(internalName) + $$$QuitOnError(sc) + } + } + $$$QuitOnError(..%Save()) + $$$QuitOnError(settingsDoc.Load()) // reload doc to update timestamps + if ($IsObject(%SourceControl)) { + $$$QuitOnError(%SourceControl.OnAfterSave(internalName)) + } + quit sc +} + Method %Save() As %Status { set sc = ..%ValidateObject() @@ -122,6 +147,45 @@ Method %Save() As %Status quit $$$OK } +Method ToDynamicObject() As %DynamicObject +{ + // uses custom methods rather than %JSON.Adaptor because Mappings multidimensional + // array is not supported + set settingsJSON = { + "pullEventClass": (..pullEventClass), + "percentClassReplace": (..percentClassReplace), + "Mappings": {} + } + do settingsJSON.%Set("decomposeProductions",..decomposeProductions,"boolean") + set k1 = $order(..Mappings("")) + while (k1 '= "") { + do settingsJSON.Mappings.%Set(k1, {}) + set k2 = $order(..Mappings(k1,"")) + while (k2 '= "") { + do settingsJSON.Mappings.%Get(k1).%Set(k2,..Mappings(k1,k2)) + set k2 = $order(..Mappings(k1, k2)) + } + set k1 = $order(..Mappings(k1)) + } + return settingsJSON +} + +Method ImportDynamicObject(pSettingsDyn As %DynamicObject) +{ + set ..pullEventClass = pSettingsDyn.%Get("pullEventClass") + set ..percentClassReplace = pSettingsDyn.%Get("percentClassReplace") + set ..decomposeProductions = pSettingsDyn.%Get("decomposeProductions") + kill ..Mappings + set mappingsDyn = pSettingsDyn.%Get("Mappings", {}) + set i1 = mappingsDyn.%GetIterator() + while i1.%GetNext(.k1, .v1) { + set i2 = v1.%GetIterator() + while i2.%GetNext(.k2, .v2) { + set ..Mappings(k1, k2) = v2 + } + } +} + ClassMethod CreateNamespaceTempFolder() As %Status { set storage = ##class(SourceControl.Git.Utils).#Storage @@ -158,7 +222,7 @@ ClassMethod Configure() As %Boolean [ CodeMode = objectgenerator ] do %code.WriteLine(" set inst."_property_" = value") } - do %code.WriteLine(" $$$ThrowOnError(inst.%Save())") + do %code.WriteLine(" $$$ThrowOnError(inst.SaveWithSourceControl())") do %code.WriteLine(" write !,""Settings saved.""") do %code.WriteLine(" do inst.OnAfterConfigure()") do %code.WriteLine(" quit 1") diff --git a/cls/SourceControl/Git/Settings/Document.cls b/cls/SourceControl/Git/Settings/Document.cls new file mode 100644 index 00000000..fe93a3fc --- /dev/null +++ b/cls/SourceControl/Git/Settings/Document.cls @@ -0,0 +1,108 @@ +/// Custom studio document type for git-source-control settings that are controlled by a file +Class SourceControl.Git.Settings.Document Extends %Studio.AbstractDocument +{ + +Projection RegisterExtension As %Projection.StudioDocument(DocumentExtension = "GSC", DocumentNew = 0, DocumentType = "json"); + +Parameter INTERNALNAME = "git-source-control.GSC"; + +Parameter EXTERNALNAME = "git-source-control.json"; + +/// Return 1 if the routine 'name' exists and 0 if it does not. +ClassMethod Exists(name As %String) As %Boolean +{ + return (name = ..#INTERNALNAME) +} + +/// Load the routine in Name into the stream Code +Method Load() As %Status +{ + set sc = $$$OK + try { + set stream = ..GetCurrentStream() + $$$ThrowOnError(..Code.CopyFromAndSave(stream)) + $$$ThrowOnError(..Code.Rewind()) + do ..UpdateHash(stream) + } catch err { + set sc = err.AsStatus() + } + return sc +} + +Method GetCurrentStream() As %Stream.Object +{ + set settings = ##class(SourceControl.Git.Settings).%New() + set dynObj = settings.ToDynamicObject() + set formatter = ##class(%JSON.Formatter).%New() + $$$ThrowOnError(formatter.FormatToStream(dynObj, .stream)) + return stream +} + +/// Save the routine stored in Code +Method Save() As %Status +{ + set sc = $$$OK + try { + try { + set settingsJSON = ##class(%DynamicObject).%FromJSON(..Code) + } catch err { + $$$ThrowStatus($$$ERROR($$$GeneralError, "Invalid JSON")) + } + set settings = ##class(SourceControl.Git.Settings).%New() + do settings.ImportDynamicObject(settingsJSON) + set sc = settings.%Save() + quit:$$$ISERR(sc) + } catch err { + set sc = err.AsStatus() + } + return sc +} + +ClassMethod ListExecute(ByRef qHandle As %Binary, Directory As %String, Flat As %Boolean, System As %Boolean) As %Status +{ + if $g(Directory)'="" { + set qHandle="" + quit $$$OK + } + set qHandle = $listbuild(1,"") + quit $$$OK +} + +ClassMethod ListFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ListExecute ] +{ + set Row="", AtEnd=0 + set rownum = $lg(qHandle,1) + if rownum'=1 { + set AtEnd = 1 + } else { + set Row = $listbuild(..#INTERNALNAME,$zts-5,0,"") + set $list(qHandle,1) = 2 + } + quit $$$OK +} + +ClassMethod ListClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ListExecute ] +{ + set qHandle = "" + quit $$$OK +} + +Method UpdateHash(stream) +{ + set stream = $Get(stream,..GetCurrentStream()) + set hash = $System.Encryption.SHA1HashStream(stream) + if $get(@##class(SourceControl.Git.Utils).#Storage@("settings","Hash")) '= hash { + set @##class(SourceControl.Git.Utils).#Storage@("settings","Hash") = hash + set @##class(SourceControl.Git.Utils).#Storage@("settings","TS") = $zdatetime($h,3) + } +} + +/// Return the timestamp of routine 'name' in %TimeStamp format. This is used to determine if the routine has +/// been updated on the server and so needs reloading from Studio. So the format should be $zdatetime($horolog,3), +/// or "" if the routine does not exist. +ClassMethod TimeStamp(name As %String) As %TimeStamp +{ + return $get(@##class(SourceControl.Git.Utils).#Storage@("settings","TS"), $zdatetime($h,3)) +} + +} diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 5c7032bb..3e2cb979 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2059,6 +2059,10 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S set relativePath = context.ResourceReference.Processor.OnItemRelativePath(InternalName) quit relativePath } + if ($zconvert(InternalName,"l") = $zconvert(##class(SourceControl.Git.Settings.Document).#INTERNALNAME,"l")) { + // git-source-control settings file will always live at repository root + quit ##class(SourceControl.Git.Settings.Document).#EXTERNALNAME + } // For an abstract document, use the GetOther() method to try to determine its "real" class if ..UserTypeCached(InternalName,.docclass,.doctype) { @@ -2219,6 +2223,9 @@ ClassMethod NameToInternalName(Name, IgnorePercent = 1, IgnoreNonexistent = 1, V } elseif ($zconvert(Name,"U")'[$zconvert($$$SourceRoot,"U")) { set Name = ..TempFolder()_Name } + if ($zconvert(Name,"l") = $zconvert(..TempFolder()_##class(SourceControl.Git.Settings.Document).#EXTERNALNAME,"l")) { + quit ##class(SourceControl.Git.Settings.Document).#INTERNALNAME + } if (##class(%File).Exists(Name)) { set InternalName = ##class(SourceControl.Git.File).ExternalNameToInternalName(Name) if (InternalName '= "") && (context.IsInGitEnabledPackage) { diff --git a/csp/gitprojectsettings.csp b/csp/gitprojectsettings.csp index 666cd9be..08da674d 100644 --- a/csp/gitprojectsettings.csp +++ b/csp/gitprojectsettings.csp @@ -75,7 +75,7 @@ body { set webuiURL = ##class(SourceControl.Git.WebUIDriver).GetURLPrefix(%request, webuiURL) set settings = ##class(SourceControl.Git.Settings).%New() - if $Data(%request.Data("gitsettings",1)) { + if (%request.Method="POST") && $Data(%request.Data("gitsettings",1)) { for param="gitUserName","gitUserEmail" { set $Property(settings,param) = $Get(%request.Data(param,1)) } @@ -120,7 +120,21 @@ body { set i = i+1 } } - do settings.%Save() + set err = "" + try { + set buffer = ##class(SourceControl.Git.Util.Buffer).%New() + do buffer.BeginCaptureOutput() + $$$ThrowOnError(settings.SaveWithSourceControl()) + do buffer.EndCaptureOutput(.out) + &html<
+
#(..EscapeHTML(out))#
+
Settings saved.
+
> + } catch err { + kill buffer + do err.Log() + &html<
An error occurred and has been logged to the application error log.
> + } }
@@ -546,9 +560,6 @@ body {
- - Settings saved. Click cross in the upper-right corner to close the settings window. -
diff --git a/test/UnitTest/SourceControl/Git/Settings.cls b/test/UnitTest/SourceControl/Git/Settings.cls new file mode 100644 index 00000000..3927da9e --- /dev/null +++ b/test/UnitTest/SourceControl/Git/Settings.cls @@ -0,0 +1,119 @@ +Class UnitTest.SourceControl.Git.Settings Extends %UnitTest.TestCase +{ + +Property SourceControlGlobal [ MultiDimensional ]; + +Property InitialExtension As %String [ InitialExpression = {##class(%Studio.SourceControl.Interface).SourceControlClassGet()} ]; + +Method SampleSettingsJSON() +{ + return { + "pullEventClass": "pull event class", + "percentClassReplace": "x", + "decomposeProductions": true, + "Mappings": { + "TUV": { + "*": "tuv/", + "UnitTest": "tuv2/" + }, + "XYZ": { + "*": "xyz/" + } + } + } +} + +Method TestJSONImportExport() +{ + set settingsDynObj = ..SampleSettingsJSON() + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.decomposeProductions = "" + set settings.percentClassReplace = "" + set settings.pullEventClass = "" + do settings.ImportDynamicObject(settingsDynObj) + do $$$AssertEquals(settings.decomposeProductions, 1) + do $$$AssertEquals(settings.percentClassReplace, "x") + do $$$AssertEquals(settings.pullEventClass, "pull event class") + do $$$AssertEquals($get(settings.Mappings("TUV","*")),"tuv/") + do $$$AssertEquals($get(settings.Mappings("TUV","UnitTest")),"tuv2/") + do $$$AssertEquals($get(settings.Mappings("XYZ","*")),"xyz/") + + $$$ThrowOnError(settings.%Save()) + set document = ##class(%RoutineMgr).%OpenId(##class(SourceControl.Git.Settings.Document).#INTERNALNAME) + set settingsDynObj = ##class(%DynamicObject).%FromJSON(document.Code) + do $$$AssertEquals(settingsDynObj.decomposeProductions, 1) + do $$$AssertEquals(settingsDynObj.percentClassReplace, "x") + do $$$AssertEquals(settingsDynObj.pullEventClass, "pull event class") + do $$$AssertEquals(settingsDynObj.Mappings."TUV"."*","tuv/") + do $$$AssertEquals(settingsDynObj.Mappings."TUV"."UnitTest","tuv2/") + do $$$AssertEquals(settingsDynObj.Mappings."XYZ"."*","xyz/") +} + +Method TestSaveAndImportSettings() +{ + // save settings + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.Mappings("CLS","Foo") = "foo/" + set settings.pullEventClass = "SourceControl.Git.PullEventHandler.Default" + set settings.percentClassReplace = "_" + set settings.decomposeProductions = 1 + $$$ThrowOnError(settings.SaveWithSourceControl()) + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).AddToSourceControl("git-source-control.GSC")) + // settings file should be in source control + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("git-source-control.GSC")) + do $$$AssertEquals($replace(##class(SourceControl.Git.Utils).ExternalName("git-source-control.GSC"),"\","/"),"git-source-control.json") + // commit settings + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Commit("git-source-control.GSC")) + // settings should be in the global + do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo/") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.Default") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"_") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"1") + // change and save settings + set settings.Mappings("CLS","Foo") = "foo2/" + set settings.pullEventClass = "SourceControl.Git.PullEventHandler.IncrementalLoad" + set settings.percentClassReplace = "x" + set settings.decomposeProductions = 0 + $$$ThrowOnError(settings.SaveWithSourceControl()) + // new setting should be in the global + do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo2/") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.IncrementalLoad") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"x") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"0") + // revert change to settings + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Revert("git-source-control.GSC")) + // old setting should be in the global + do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo/") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.Default") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"_") + do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"1") +} + +Method OnBeforeAllTests() As %Status +{ + merge ..SourceControlGlobal = ^SYS("SourceControl") + return $$$OK +} + +Method OnBeforeOneTest() As %Status +{ + kill ^SYS("SourceControl") + do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension") + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"dir" + $$$ThrowOnError(settings.%Save()) + set workMgr = $System.WorkMgr.%New("") + $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) + $$$ThrowOnError(workMgr.WaitForComplete()) + quit $$$OK +} + +Method %OnClose() As %Status +{ + do ##class(%Studio.SourceControl.Interface).SourceControlClassSet(..InitialExtension) + kill ^SYS("SourceControl") + merge ^SYS("SourceControl") = ..SourceControlGlobal + quit $$$OK +} + +} From 9fb21042d166283631f634922b8c776378428f0e Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:16:37 -0400 Subject: [PATCH 58/79] docs: documentation and testing plan for production decomposition --- docs/production-decomposition.md | 12 ++++++++++++ docs/testing.md | 16 +++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 docs/production-decomposition.md diff --git a/docs/production-decomposition.md b/docs/production-decomposition.md new file mode 100644 index 00000000..28dbd904 --- /dev/null +++ b/docs/production-decomposition.md @@ -0,0 +1,12 @@ +# Production Decomposition +Production decomposition is a feature of git-source-control that allows multiple developers to edit the same IRIS Interoperability production in the same namespace. In the past, the production class has been an obstacle preventing organizations using multi-user development namespaces from adopting source control. Production decomposition resolves this by representing the production as a directory of files for each production item that may be edited independently. An uncommitted change to the settings for a single item through the Interoperability Portal will block other users from editing that item while allowing changes to other items in the production. + +## Enabling production decomposition +The feature may be enabled by checking the "Decompose Productions" box in the Git Settings page. For deployment of changes to other environments through git to work properly, the value of this setting must match on all namespaces connected to this repository. To assist, settings are automatically exported into a `git-source-control.json` file at the root of the repository that may be committed and imported into other environments. + +If there are existing productions in the namespace, they should be migrated to the new decomposed format by running `do ##class(SourceControl.Git.API).BaselineProductions()`. You may then use the Git Web UI to view, commit, and push the corresponding changes. This method should be run in a single namespace and then deployed to other namespaces through normal git-source-control deployment mechanisms. + +## Known Limitations +- The source control hooks for production decomposition are currently only supported when editing via the Interoperability Portal. Editing the production class directly in VS Code or Studio may overwrite other users' changes. +- Any custom methods, parameters, etc. in the production class will not be source controlled if production decomposition is enabled. A recommended workaround is to move these items to a separate utility class. +- Production decomposition is not supported for deployment of changes to productions using the InterSystems Package Manager. diff --git a/docs/testing.md b/docs/testing.md index b72f4316..9759e175 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -18,4 +18,18 @@ The following is a testing plan that should be followed prior to release of a ne - Add, edit, and delete items through Studio / VS Code. Use the Sync option. All changes should be committed and pushed to the remote. - Add, edit, and delete items on the remote. Add, edit, and delete unrelated items through Studio/VSCode. All changes should be pulled, committed, and pushed. - Add an item to an interoperability production and sync. Check out a new feature branch. The item should no longer exist in the production. Set the previous branch as the remote merge branch. Sync. The new item should exist in the production. - - Add an item to a production and sync. Check out a new feature branch. The item should no longer exist in the production. Set the previous branch as the remote merge branch. Add a new item to the production. Sync. The production should now have both new items, and the source control output should show it automatically resolved a conflict. \ No newline at end of file + - Add an item to a production and sync. Check out a new feature branch. The item should no longer exist in the production. Set the previous branch as the remote merge branch. Add a new item to the production. Sync. The production should now have both new items, and the source control output should show it automatically resolved a conflict. + + ## Testing production decomposition + - Enable production decomposition in the git-source-control settings, and add a mapping for PTD items to the /ptd subdirectory. + - In Basic mode, check out a new branch. Create a new production, add some items, and sync. Confirm a file for production settings and a file for each production item has been added to the /ptd subdirectory and pushed to the remote repository. + - In Advanced mode, create a new user. Log in and modify some items on the production. As the previous user, try to modify items in the production. I should not be able to modify those items modified by the other users. + - Revert some production items through the workspace view in the Web UI. The production should automatically update. + - In Basic mode, test deployment: + - Create a new namespace and enable basic mode and production decomposition. Set the default merge branch to the branch checked out on the other namespace. Sync and confirm that the new production has been created with all expected items. + - On the original namespace, delete and modify some items from the production, then sync. On the second namespace, sync again. The items should be deleted and modified to match. + - Test migration of a production to decomposed format: + - On the initial namespace, disable production decomposition. Create a new production and add a number of items. Sync and confirm it has been pushed to the remote repository. + - On the second namespace, sync and confirm the new production has been created. + - On the initial namespace, turn on production decomposition. From terminal, run `do ##class(SourceControl.Git.API).BaselineProductions()`. Confirm the Web UI includes changes for delete of the old production class and adds for all production items. Commit all items and push the branch. + - On the second namespace, turn on production decomposition. Sync. The production should be reloaded with no changes. \ No newline at end of file From 4ea1312ae27737ab179ca1c6be6b297b4663a651 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:10:15 -0400 Subject: [PATCH 59/79] enabling production decomposition creates a default mapping for PTD items --- cls/SourceControl/Git/Settings.cls | 3 +++ docs/testing.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index bb26b227..57767cc6 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -144,6 +144,9 @@ Method %Save() As %Status set ..userBasicMode = ##class(SourceControl.Git.Utils).UserBasicMode() set @storage@("settings","decomposeProductions") = ..decomposeProductions + if (..decomposeProductions & ($data(..Mappings("PTD"))<10)) { + set ..Mappings("PTD","*") = "ptd/" // configure a default mapping for PTD items + } kill @##class(SourceControl.Git.Utils).MappingsNode() merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings diff --git a/docs/testing.md b/docs/testing.md index 9759e175..767edaf2 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -21,7 +21,7 @@ The following is a testing plan that should be followed prior to release of a ne - Add an item to a production and sync. Check out a new feature branch. The item should no longer exist in the production. Set the previous branch as the remote merge branch. Add a new item to the production. Sync. The production should now have both new items, and the source control output should show it automatically resolved a conflict. ## Testing production decomposition - - Enable production decomposition in the git-source-control settings, and add a mapping for PTD items to the /ptd subdirectory. + - Enable production decomposition in the git-source-control settings. - In Basic mode, check out a new branch. Create a new production, add some items, and sync. Confirm a file for production settings and a file for each production item has been added to the /ptd subdirectory and pushed to the remote repository. - In Advanced mode, create a new user. Log in and modify some items on the production. As the previous user, try to modify items in the production. I should not be able to modify those items modified by the other users. - Revert some production items through the workspace view in the Web UI. The production should automatically update. From fc8aa96ecae6d27d2201acf6f7d7b5d2c6731696 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Mon, 30 Sep 2024 15:08:31 -0400 Subject: [PATCH 60/79] fix issues with settings file on repo initialization --- cls/SourceControl/Git/Settings.cls | 31 +++++++++++++++++------------- cls/SourceControl/Git/Utils.cls | 12 +++--------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 87f3ed90..b3014ad1 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -79,20 +79,25 @@ Method SaveWithSourceControl() As %Status if '$isobject($get(%SourceControl)) { do ##class(%Studio.SourceControl.Interface).SourceControlCreate($username) } - // Source control settings naively by only calling OnAfterSave hooks - // Future enhancement: add full source control support with settings UI + set gitDir = ##class(%File).NormalizeDirectory(..namespaceTemp)_".git" + set skipSourceControl = ('##class(%File).DirectoryExists(gitDir) && + (##class(%Studio.SourceControl.Interface).SourceControlClassGet() = ##class(SourceControl.Git.Extension).%ClassName(1))) + $$$QuitOnError(..%Save()) set internalName = ##class(SourceControl.Git.Settings.Document).#INTERNALNAME set settingsDoc = ##class(SourceControl.Git.Settings.Document).%New(internalName) - if (##class(%Studio.SourceControl.Interface).SourceControlClassGet() = ##class(SourceControl.Git.Extension).%ClassName(1)) { - if '##class(SourceControl.Git.Utils).IsInSourceControl(internalName) { - set sc = ##class(SourceControl.Git.Utils).AddToSourceControl(internalName) - $$$QuitOnError(sc) - } - } - $$$QuitOnError(..%Save()) $$$QuitOnError(settingsDoc.Load()) // reload doc to update timestamps - if ($IsObject(%SourceControl)) { - $$$QuitOnError(%SourceControl.OnAfterSave(internalName)) + if 'skipSourceControl { + // Source control settings naively by only calling OnAfterSave hooks + // Future enhancement: add full source control support with settings UI + if (##class(%Studio.SourceControl.Interface).SourceControlClassGet() = ##class(SourceControl.Git.Extension).%ClassName(1)) { + if '##class(SourceControl.Git.Utils).IsInSourceControl(internalName) { + set sc = ##class(SourceControl.Git.Utils).AddToSourceControl(internalName) + $$$QuitOnError(sc) + } + } + if ($IsObject(%SourceControl)) { + $$$QuitOnError(%SourceControl.OnAfterSave(internalName)) + } } quit sc } @@ -376,8 +381,6 @@ Method OnAfterConfigure() As %Boolean set workMgr = $System.WorkMgr.%New("") $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init")) $$$ThrowOnError(workMgr.WaitForComplete()) - - do ##class(SourceControl.Git.Utils).EmptyInitialCommit() } elseif (value = 2) { set response = ##class(%Library.Prompt).GetString("Git remote URL (note: if authentication is required, use SSH, not HTTPS):",.remote,,,,defaultPromptFlag) if (response '= $$$SuccessResponse) { @@ -390,6 +393,8 @@ Method OnAfterConfigure() As %Boolean set workMgr = $System.WorkMgr.%New("") $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Clone",remote)) $$$ThrowOnError(workMgr.WaitForComplete()) + // export settings file without committing + $$$ThrowOnError(..SaveWithSourceControl()) } } } diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 4d3ea117..039fd960 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -337,8 +337,9 @@ ClassMethod Init() As %Status { do ..RunGitCommand("init",.errStream,.outStream) do ..PrintStreams(outStream, errStream) - - quit $$$OK + set settings = ##class(SourceControl.Git.Settings).%New() + $$$QuitOnError(settings.SaveWithSourceControl()) + quit ..Commit(##class(SourceControl.Git.Settings.Document).#INTERNALNAME,"initial commit") } ClassMethod Revert(InternalName As %String) As %Status @@ -2097,12 +2098,6 @@ ClassMethod GitStatus(ByRef files, IncludeAllFiles = 0) } } -ClassMethod EmptyInitialCommit() -{ - set ret = ..RunGitCommandWithInput("commit",, .errStream, .outStream, "--allow-empty", "-m", "empty initial commit") - do ..PrintStreams(errStream, outStream) -} - /* Internal name: e.g. SourceControl.Git.Utils.CLS External name e.g. cls/SourceControl/Git/Utils.cls @@ -2772,4 +2767,3 @@ ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status } } - From 53c5985929a5a72b26553733ecc1316f8d7dbc0e Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:38:41 -0400 Subject: [PATCH 61/79] fix: settings import/export supports NoFolders --- cls/SourceControl/Git/Settings.cls | 10 ++++++++-- test/UnitTest/SourceControl/Git/Settings.cls | 21 ++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index b3014ad1..73129775 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -177,7 +177,10 @@ Method ToDynamicObject() As %DynamicObject do settingsJSON.Mappings.%Set(k1, {}) set k2 = $order(..Mappings(k1,"")) while (k2 '= "") { - do settingsJSON.Mappings.%Get(k1).%Set(k2,..Mappings(k1,k2)) + do settingsJSON.Mappings.%Get(k1).%Set(k2,{"directory": (..Mappings(k1,k2))}) + if $get(..Mappings(k1,k2,"NoFolders")) { + do settingsJSON.Mappings.%Get(k1).%Get(k2).%Set("noFolders", 1, "boolean") + } set k2 = $order(..Mappings(k1, k2)) } set k1 = $order(..Mappings(k1)) @@ -196,7 +199,10 @@ Method ImportDynamicObject(pSettingsDyn As %DynamicObject) while i1.%GetNext(.k1, .v1) { set i2 = v1.%GetIterator() while i2.%GetNext(.k2, .v2) { - set ..Mappings(k1, k2) = v2 + set ..Mappings(k1, k2) = v2.%Get("directory") + if v2.%Get("noFolders") { + set ..Mappings(k1, k2, "NoFolders") = 1 + } } } } diff --git a/test/UnitTest/SourceControl/Git/Settings.cls b/test/UnitTest/SourceControl/Git/Settings.cls index 3927da9e..3334a3b9 100644 --- a/test/UnitTest/SourceControl/Git/Settings.cls +++ b/test/UnitTest/SourceControl/Git/Settings.cls @@ -13,11 +13,18 @@ Method SampleSettingsJSON() "decomposeProductions": true, "Mappings": { "TUV": { - "*": "tuv/", - "UnitTest": "tuv2/" + "*": { + "directory": "tuv/" + }, + "UnitTest": { + "directory": "tuv2/", + "noFolders": true + } }, "XYZ": { - "*": "xyz/" + "*": { + "directory": "xyz/" + } } } } @@ -36,6 +43,7 @@ Method TestJSONImportExport() do $$$AssertEquals(settings.pullEventClass, "pull event class") do $$$AssertEquals($get(settings.Mappings("TUV","*")),"tuv/") do $$$AssertEquals($get(settings.Mappings("TUV","UnitTest")),"tuv2/") + do $$$AssertTrue($get(settings.Mappings("TUV","UnitTest","NoFolders"))) do $$$AssertEquals($get(settings.Mappings("XYZ","*")),"xyz/") $$$ThrowOnError(settings.%Save()) @@ -44,9 +52,10 @@ Method TestJSONImportExport() do $$$AssertEquals(settingsDynObj.decomposeProductions, 1) do $$$AssertEquals(settingsDynObj.percentClassReplace, "x") do $$$AssertEquals(settingsDynObj.pullEventClass, "pull event class") - do $$$AssertEquals(settingsDynObj.Mappings."TUV"."*","tuv/") - do $$$AssertEquals(settingsDynObj.Mappings."TUV"."UnitTest","tuv2/") - do $$$AssertEquals(settingsDynObj.Mappings."XYZ"."*","xyz/") + do $$$AssertEquals(settingsDynObj.Mappings."TUV"."*".directory,"tuv/") + do $$$AssertEquals(settingsDynObj.Mappings."TUV"."UnitTest".directory,"tuv2/") + do $$$AssertTrue(settingsDynObj.Mappings."TUV"."UnitTest".noFolders) + do $$$AssertEquals(settingsDynObj.Mappings."XYZ"."*".directory,"xyz/") } Method TestSaveAndImportSettings() From d04fa4e3891036fe5130fff60d717f185054985e Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:47:25 -0400 Subject: [PATCH 62/79] Port production changes from CCR --- cls/SourceControl/Git/Production.cls | 95 ++++++++++++++++++++++++---- 1 file changed, 81 insertions(+), 14 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 0f026872..a7783346 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -35,6 +35,37 @@ ClassMethod ExportProductionDefinitionShards(productionClass As %String, nameMet Return $$$OK } +/// Removes settings for a given Production and each Config Item from +/// source control. This marks each of them for delete and is triggered +/// by deleting a decomposed Production. +ClassMethod DeleteProductionDefinitionShards(productionClass As %String, deleteMethod As %String, nameMethod As %String) As %Status +{ + set sc = $$$OK + if '$isobject($get(%SourceControl)) { + new %SourceControl + $$$ThrowOnError(##class(%Studio.SourceControl.Interface).SourceControlCreate()) + } + set settingsPTD = ..CreateInternalName(productionClass,,,1) + set sourceControlClass = ##class(%Studio.SourceControl.Interface).SourceControlClassGet() + set settingsPTDFile = $classmethod(sourceControlClass, nameMethod, settingsPTD) + // if the Production settings PTD exists, delete all PTDs for this Production + if ##class(%File).Exists(settingsPTDFile) { + set ptdDir = ##class(%File).GetDirectory(settingsPTDFile) + set rs = ##class(%ResultSet).%New("%File:FileSet") + $$$ThrowOnError(rs.Execute(ptdDir, "*.xml")) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + while rs.Next() { + set ptdFilename = rs.Data("Name") + set sc = ##class(%Studio.SourceControl.Production).ParseExternalName(ptdFilename, .ptdInternalName) + quit:$$$ISERR(sc) + // TODO: Consider reverting delete if any ptd is not editable by current user + set sc = $method(%SourceControl, deleteMethod, ptdInternalName) + quit:$$$ISERR(sc) + } + } + return sc +} + /// Exports a Studio project including both the provided PTD and export notes for the PTD ClassMethod ExportProjectForPTD(productionClass, ptdName, exportPath) As %Status { @@ -101,6 +132,15 @@ ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status Return sc } +/// Imports a PTD into a produciton given an external name and produciton name +ClassMethod ImportPTD(externalName As %String, productionName As %String) As %Status +{ + set rollbackFile = ##class(%File).TempFilename() + set sc = ##class(Ens.Deployment.Deploy).DeployCode(externalName,productionName,0,rollbackFile) + do ##class(%File).Delete(rollbackFile) + return sc +} + /// Export a single Production Config Item. For a given Ens.Config.Item, the /// exports the PTD for this item to the file system under the directory specified ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String, Output internalName As %String) As %Status @@ -164,7 +204,7 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt set modifiedInternalName = ..CreateInternalName(productionName,,,1) } } - if (modifiedInternalName '= "") { + if ($get(modifiedInternalName) '= "") { set modifiedItems(modifiedInternalName) = "M" } } else { @@ -234,10 +274,18 @@ ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %B if (className '= "") && $$$comClassDefined(className) { return $classmethod(className, "%Extends", "Ens.Production") } else { + // check if there exists a Production settings PTD export for ths Production + set settingsPTD = ..CreateInternalName(className,,,1) + set settingsPTDFilename = $classmethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, settingsPTD) + if ##class(%File).Exists(settingsPTDFilename) { + return 1 + } + // check if there is a class export for this Production, load it for class definition set filename = $classmethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, className_".CLS") if ##class(%File).Exists(filename) { $$$ThrowOnError($System.OBJ.Load(filename)) } + // if Production exists as a class definition on the server, check if extending Ens.Production set classDef = ##class(%Dictionary.ClassDefinition).%OpenId(className) if $isobject(classDef) && ##class(%Dictionary.ClassDefinition).%ExistsId(classDef.Super) { return $classmethod(classDef.Super, "%Extends", "Ens.Production") @@ -247,25 +295,31 @@ ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %B } /// Given a file name for a PTD item, returns a suggested internal name. This method assumes that the file exists on disk. -ClassMethod ParseExternalName(externalName, Output internalName = "", Output productionName = "") +ClassMethod ParseExternalName(externalName, Output internalName = "", Output productionName = "") As %Status { - if ##class(%File).Exists(externalName) { - set file = $piece(externalName, "/", *) - set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(externalName) - set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") - set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) - set productionName = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") - if $extract(file,1,9) = "ProdStgs-" { - set internalName = ..CreateInternalName(productionName,,,1) - } else { - // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name + set sc = $$$OK + set extNameNormalized = $replace(externalName, "\", "/") + set file = $piece(extNameNormalized, "/", *) + if $extract(file,1,9) = "ProdStgs-" { + set productionName = $replace($extract(file,10,*-4), "_", ".") + set internalName = ..CreateInternalName(productionName,,,1) + } else { + if ##class(%File).Exists(externalName) { + // Special case for Config Item Settings PTD, requires checking PTD CDATA for Item and Class name + set deployDoc = ##class(EnsLib.EDI.XML.Document).%New(externalName) + set exportNotesPTDText = $ZCVT(deployDoc.GetValueAt("/Export/Document[1]/1"),"I","XML") + set exportNotesPTD = ##class(EnsLib.EDI.XML.Document).%New(exportNotesPTDText) + set productionName = exportNotesPTD.GetValueAt("/Deployment/Creation/SourceProduction") set settingsPTDText = $zconvert(deployDoc.GetValueAt("/Export/Document[2]/1"),"I","XML") set settingsPTD = ##class(EnsLib.EDI.XML.Document).%New(settingsPTDText) set itemClass = settingsPTD.GetValueAt("/Item/@ClassName") set itemName = settingsPTD.GetValueAt("/Item/@Name") set internalName = ..CreateInternalName(productionName, itemName, itemClass, 0) - } + } else { + set sc = $$$ERROR($$$GeneralError, "Item settings PTD file " _ externalName _ " does not exist. Cannot parse external name.") + } } + return sc } /// Given an internal name for a PTD item, returns a suggested filename for export, as well as: @@ -295,7 +349,7 @@ ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fi } /// Calculates the internal name for a decomposed production item -ClassMethod CreateInternalName(productionName = "", itemName = "", itemClassName = "", isProductionSettings As %Boolean = 0) +ClassMethod CreateInternalName(productionName = "", itemName = "", itemClassName = "", isProductionSettings As %Boolean = 0) As %String { return $select( isProductionSettings: productionName_"||ProductionSettings-"_productionName_".PTD", @@ -370,4 +424,17 @@ ClassMethod CreateProduction(productionName As %String, superClasses = "") As %S return sc } +/// Given an internal name of a Production and an IRIS user, populate an array with the filenames +/// for each of their current uncommitted changes associated with the given Production +ClassMethod GetUserProductionChanges(productionName As %String, ByRef items) +{ + set sql = "SELECT InternalName, Action FROM %Studio_SourceControl.Change WHERE ChangedBy = ? AND Committed = 0 AND InternalName %STARTSWITH ?" + set rs = ##class(%SQL.Statement).%ExecDirect(,sql,$username,productionName_"||") + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + while rs.%Next() { + set items(rs.InternalName) = rs.Action + } + quit $$$OK +} + } From 7fa18724516b8a7d76dafe7b38596ab03a7f844d Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:03:24 -0400 Subject: [PATCH 63/79] fix: privilege violation with getting modified production items --- cls/SourceControl/Git/Production.cls | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index a7783346..b7ea789e 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -216,7 +216,7 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt merge ^IRIS.Temp("sscProd",$job,"modifiedItems") = modifiedItems // FUTURE: use a percent variable or PPG instead kill ^IRIS.Temp("sscProd",$job,"items") - set rs = ##class(%SQL.Statement).%ExecDirect( + set rs = ##class(%SQL.Statement).%ExecDirectNoPriv( ,"select Name, ClassName from Ens_Config.Item where Production = ?" , productionName) $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) @@ -231,7 +231,7 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) set productionName = $piece(internalName,".",1,*-1) if ..IsEnsPortal() { // If adding/deleting from SMP, get the modified items by comparing items in temp global with items now - set rs = ##class(%SQL.Statement).%ExecDirect( + set rs = ##class(%SQL.Statement).%ExecDirectNoPriv( ,"select Name, ClassName from Ens_Config.Item where Production = ?" , productionName) $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) From aaa13362757288d0cb1b32693d4e71aad1f8af60 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:47:43 -0400 Subject: [PATCH 64/79] docs: standardize casing of Production Decomposition feature name --- docs/production-decomposition.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/production-decomposition.md b/docs/production-decomposition.md index 28dbd904..14d03df4 100644 --- a/docs/production-decomposition.md +++ b/docs/production-decomposition.md @@ -1,5 +1,5 @@ # Production Decomposition -Production decomposition is a feature of git-source-control that allows multiple developers to edit the same IRIS Interoperability production in the same namespace. In the past, the production class has been an obstacle preventing organizations using multi-user development namespaces from adopting source control. Production decomposition resolves this by representing the production as a directory of files for each production item that may be edited independently. An uncommitted change to the settings for a single item through the Interoperability Portal will block other users from editing that item while allowing changes to other items in the production. +Production Decomposition is a feature of git-source-control that allows multiple developers to edit the same IRIS Interoperability production in the same namespace. In the past, the production class has been an obstacle preventing organizations using multi-user development namespaces from adopting source control. Production Decomposition resolves this by representing the production as a directory of files for each production item that may be edited independently. An uncommitted change to the settings for a single item through the Interoperability Portal will block other users from editing that item while allowing changes to other items in the production. ## Enabling production decomposition The feature may be enabled by checking the "Decompose Productions" box in the Git Settings page. For deployment of changes to other environments through git to work properly, the value of this setting must match on all namespaces connected to this repository. To assist, settings are automatically exported into a `git-source-control.json` file at the root of the repository that may be committed and imported into other environments. @@ -7,6 +7,6 @@ The feature may be enabled by checking the "Decompose Productions" box in the Gi If there are existing productions in the namespace, they should be migrated to the new decomposed format by running `do ##class(SourceControl.Git.API).BaselineProductions()`. You may then use the Git Web UI to view, commit, and push the corresponding changes. This method should be run in a single namespace and then deployed to other namespaces through normal git-source-control deployment mechanisms. ## Known Limitations -- The source control hooks for production decomposition are currently only supported when editing via the Interoperability Portal. Editing the production class directly in VS Code or Studio may overwrite other users' changes. -- Any custom methods, parameters, etc. in the production class will not be source controlled if production decomposition is enabled. A recommended workaround is to move these items to a separate utility class. -- Production decomposition is not supported for deployment of changes to productions using the InterSystems Package Manager. +- The source control hooks for Production Decomposition are currently only supported when editing via the Interoperability Portal. Editing the production class directly in VS Code or Studio may overwrite other users' changes. +- Any custom methods, parameters, etc. in the production class will not be source controlled if Production Decomposition is enabled. A recommended workaround is to move these items to a separate utility class. +- Production Decomposition is not supported for deployment of changes to productions using the InterSystems Package Manager. From 6cd7b3bdad9c43416856bc152638e87220839248 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:55:51 -0400 Subject: [PATCH 65/79] fix: production decomp SQL privilege violations and backwards incompatibilities --- cls/SourceControl/Git/Production.cls | 45 ++++++++++++++++++++-------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index b7ea789e..b38a1c61 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -16,7 +16,7 @@ ClassMethod ExportProductionDefinitionShards(productionClass As %String, nameMet Set internalNames(internalName) = 1 // next, export each item to a separate file - Set rs = ##class(%SQL.Statement).%ExecDirect(, + Set rs = ..ExecDirectNoPriv( "select Name, ClassName from Ens_Config.Item where Production = ?" , productionClass ) @@ -51,10 +51,18 @@ ClassMethod DeleteProductionDefinitionShards(productionClass As %String, deleteM // if the Production settings PTD exists, delete all PTDs for this Production if ##class(%File).Exists(settingsPTDFile) { set ptdDir = ##class(%File).GetDirectory(settingsPTDFile) - set rs = ##class(%ResultSet).%New("%File:FileSet") - $$$ThrowOnError(rs.Execute(ptdDir, "*.xml")) - $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) - while rs.Next() { + set statement = ##class(%SQL.Statement).%New() + try { + // execute without priv checking if possible on this IRIS version + set sc = statement.%PrepareClassQuery("%File","FileSet",0) + } catch err { + set sc = statement.%PrepareClassQuery("%File","FileSet") + } + quit:$$$ISERR(sc) + set rs = statement.%Execute(ptdDir, "*.xml") + throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) + while rs.%Next(.sc) { + quit:$$$ISERR(sc) set ptdFilename = rs.Data("Name") set sc = ##class(%Studio.SourceControl.Production).ParseExternalName(ptdFilename, .ptdInternalName) quit:$$$ISERR(sc) @@ -216,10 +224,10 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt merge ^IRIS.Temp("sscProd",$job,"modifiedItems") = modifiedItems // FUTURE: use a percent variable or PPG instead kill ^IRIS.Temp("sscProd",$job,"items") - set rs = ##class(%SQL.Statement).%ExecDirectNoPriv( - ,"select Name, ClassName from Ens_Config.Item where Production = ?" + set rs = ..ExecDirectNoPriv( + "select Name, ClassName from Ens_Config.Item where Production = ?" , productionName) - $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) while rs.%Next() { set ^IRIS.Temp("sscProd",$job,"items",$listbuild(rs.Name, rs.ClassName)) = 1 } @@ -231,10 +239,10 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) set productionName = $piece(internalName,".",1,*-1) if ..IsEnsPortal() { // If adding/deleting from SMP, get the modified items by comparing items in temp global with items now - set rs = ##class(%SQL.Statement).%ExecDirectNoPriv( - ,"select Name, ClassName from Ens_Config.Item where Production = ?" + set rs = ..ExecDirectNoPriv( + "select Name, ClassName from Ens_Config.Item where Production = ?" , productionName) - $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) while rs.%Next() { if '$get(^IRIS.Temp("sscProd",$job,"items", $listbuild(rs.Name, rs.ClassName))) { set itemInternalName = ..CreateInternalName(productionName, rs.Name, rs.ClassName, 0) @@ -429,12 +437,23 @@ ClassMethod CreateProduction(productionName As %String, superClasses = "") As %S ClassMethod GetUserProductionChanges(productionName As %String, ByRef items) { set sql = "SELECT InternalName, Action FROM %Studio_SourceControl.Change WHERE ChangedBy = ? AND Committed = 0 AND InternalName %STARTSWITH ?" - set rs = ##class(%SQL.Statement).%ExecDirect(,sql,$username,productionName_"||") - $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + set rs = ..ExecDirectNoPriv(sql,$username,productionName_"||") + throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message) while rs.%Next() { set items(rs.InternalName) = rs.Action } quit $$$OK } +/// Executes a SQL query without privilege checking if possible on this IRIS version +ClassMethod ExecDirectNoPriv(sql, args...) As %SQL.StatementResult +{ + try { + set rs = ##class(%SQL.Statement).%ExecDirectNoPriv(,sql,args...) + } catch err { + set rs = ##class(%SQL.Statement).%ExecDirect(,sql,args...) + } + return rs +} + } From 7df8d2cb5bf0d32b342dc773bd9ae7772a64df65 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:16:16 -0400 Subject: [PATCH 66/79] fix: issue importing production items when namespace has multiple productions ported from CCR --- cls/SourceControl/Git/Production.cls | 39 +++++++++++++++++++++++----- cls/SourceControl/Git/Utils.cls | 2 +- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index b38a1c61..5447fc48 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -149,6 +149,34 @@ ClassMethod ImportPTD(externalName As %String, productionName As %String) As %St return sc } +/// Imports all PTDs within a given directory. Also recursively imports from all subdirectories +ClassMethod ImportPTDsDir(directory As %String, isDecompMethod As %String = "") As %Status +{ + set sc = $$$OK + set rs = ##class(%ResultSet).%New("%File:FileSet") + $$$ThrowOnError(rs.Execute(directory, "*.xml", "", 1)) + $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + while rs.Next() { + set path = rs.Data("Name") + set type = rs.Data("Type") + if type = "D" { + set sc = ..ImportPTDsDir(path) + } else { + $$$ThrowOnError(..ParseExternalName(path, .internalName, .prodName)) + set srcCtrlCls = ##class(%Studio.SourceControl.Interface).SourceControlClassGet() + set isDecomp = $select(isDecompMethod="":1, 1:$classMethod(srcCtrlCls, isDecompMethod, internalName)) + if isDecomp { + set filename = ##class(%File).GetFilename(path) + if ($extract(filename) = "P") && '$$$comClassDefined(prodName) { + $$$ThrowOnError(..CreateProduction(prodName)) + } + set sc = ..ImportPTD(path, prodName) + } + } + } + return sc +} + /// Export a single Production Config Item. For a given Ens.Config.Item, the /// exports the PTD for this item to the file system under the directory specified ClassMethod ExportConfigItemSettings(productionClass As %String, item As %RegisteredObject, nameMethod As %String, Output internalName As %String) As %Status @@ -270,10 +298,7 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) /// Check if current CSP session is EnsPortal page ClassMethod IsEnsPortal() As %Boolean { - If $IsObject($Get(%session)) && ($Get(%request.Data("pageclass","1")) [ "EnsPortal") { - Return 1 - } - Return 0 + Return $Data(%request) && '($IsObject(%request) && (%request.UserAgent [ "Code")) } /// Perform check if Production Decomposition logic should be used for given item @@ -334,7 +359,7 @@ ClassMethod ParseExternalName(externalName, Output internalName = "", Output pro /// - itemName: name of the configuration item /// - productionName: name of the associated production /// - isProdSettings: if true, this item is a production settings; if false, this item is a configuration item settings -ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fileName, Output itemName, Output productionName, Output isProdSettings As %Boolean) +ClassMethod ParseInternalName(internalName, noFolders As %Boolean = 0, Output fileName, Output itemName, Output itemClassName, Output productionName, Output isProdSettings As %Boolean) { set name = $piece(internalName,".",1,*-1) if 'noFolders { @@ -394,11 +419,11 @@ ClassMethod RemoveItem(internalName, noFolders As %Boolean = 0) As %Status if '##class(%Library.EnsembleMgr).IsEnsembleNamespace() { quit } - do ..ParseInternalName(internalName, noFolders, , .itemName, .productionName, .isProdSettings) + do ..ParseInternalName(internalName, noFolders, , .itemName, .itemClassName, .productionName, .isProdSettings) if 'isProdSettings { set production = ##class(Ens.Config.Production).%OpenId(productionName,,.sc) quit:$$$ISERR(sc) - set configItem = production.OpenItemByConfigName(itemName,.sc) + set configItem = ##class(Ens.Config.Production).OpenItemByConfigName(productionName_"||"_itemName_"|"_itemClassName,.sc) quit:$$$ISERR(sc) do production.RemoveItem(configItem) set sc = production.%Save() diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index f857d531..52c3d6fb 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1345,7 +1345,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A // ideally we could just new %SourceControl, but Ens portal config pages do not use %SourceControl new %gscSkipSaveHooks set %gscSkipSaveHooks = 1 - do ##class(SourceControl.Git.Production).ParseInternalName(InternalName,,,,.targetProduction) + do ##class(SourceControl.Git.Production).ParseInternalName(InternalName,,,,,.targetProduction) if (targetProduction '= "") && '$$$comClassDefined(targetProduction) { set sc = ##class(SourceControl.Git.Production).CreateProduction(targetProduction) } From 3a375f0f0b3caea9a6e8208944ebd00d19c8d59d Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 16 Oct 2024 16:52:04 -0400 Subject: [PATCH 67/79] enh: partial support for editing decomposed production from IDE --- cls/SourceControl/Git/Production.cls | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 5447fc48..113a1275 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -211,10 +211,10 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt { kill modifiedItems set productionName = $piece(internalName,".",1,*-1) + set productionConfig = ##class(Ens.Config.Production).%OpenId(productionName) if ..IsEnsPortal() { // If editing from SMP, get the modified items by looking at %IsModified on the items in the production in memory. // No way to know if an item has been added or deleted, so ignore it. - set productionConfig = ##class(Ens.Config.Production).%OpenId(productionName) if $isobject(productionConfig) { set modifiedItem = $$$NULLOREF for i=1:1:productionConfig.Items.Count() { @@ -244,8 +244,15 @@ ClassMethod GetModifiedItemsBeforeSave(internalName, Location, Output modifiedIt set modifiedItems(modifiedInternalName) = "M" } } else { - // If editing/adding/deleting from Studio, get the modified items by comparing the XDATA in Location with the XDATA in the compiled class. - // FUTURE: implement this to support Studio + // FUTURE: get the actually modified items by comparing the XDATA in Location with the XDATA in the compiled class + // If making changes from Studio, list every item in the production. + if $isobject(productionConfig) { + set modifiedItems(..CreateInternalName(productionName,,,1)) = "M" + for i=1:1:productionConfig.Items.Count() { + set item = productionConfig.Items.GetAt(i) + set modifiedItems(..CreateInternalName(productionName, item.Name, item.ClassName, 0)) = "M" + } + } } // populate data for use in OnAfterSave kill ^IRIS.Temp("sscProd",$job,"modifiedItems") @@ -290,8 +297,16 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) merge modifiedItems = ^IRIS.Temp("sscProd",$job,"modifiedItems") } } else { - // If editing/adding/deleting from Studio, get the modified items from a percent variable set in OnBeforeSave. - // FUTURE: implement this to support Studio. + // If editing in the IDE, list every item in the production. + // FUTURE: get the actually modified items using the temp global set in OnBeforeSave + set productionConfig = ##class(Ens.Config.Production).%OpenId(productionName) + if $isobject(productionConfig) { + set modifiedItems(..CreateInternalName(productionName,,,1)) = "M" + for i=1:1:productionConfig.Items.Count() { + set item = productionConfig.Items.GetAt(i) + set modifiedItems(..CreateInternalName(productionName, item.Name, item.ClassName, 0)) = "M" + } + } } } From 201ccd73944c332457d1d2e3ccfe2fb6dc4a313f Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:02:27 -0400 Subject: [PATCH 68/79] remove last modified timestamp from PTD files after export --- cls/SourceControl/Git/Production.cls | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 113a1275..1d77be28 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -111,6 +111,22 @@ ClassMethod ExportProjectForPTD(productionClass, ptdName, exportPath) As %Status set projContentsList(exportNotesPTDName_".PTD") = "" set projContentsList(project.Name_".PRJ") = "" $$$ThrowOnError($System.OBJ.Export(.projContentsList, exportPath, "/diffexport=1")) + // remove the LastModified timestamp from the exported file + set fileStream = ##class(%Stream.FileCharacter).%OpenId(exportPath,,.st) + $$$ThrowOnError(st) + set fileCopy = ##class(%Stream.TmpCharacter).%New() + set timestampFound = 0 + while 'fileStream.AtEnd { + set line = fileStream.ReadLine() + set regex = ##class(%Regex.Matcher).%New("\") + if 'timestampFound && regex.Match(line) { + set timestampFound = 1 + set timestamp = regex.Group(1) + set line = $replace(line, timestamp, "1841-01-01 00:00:00.0000000") + } + do fileCopy.WriteLine(line) + } + $$$ThrowOnError(fileStream.CopyFromAndSave(fileCopy)) } catch err { set st = err.AsStatus() } From c34fa582b8159106dc7f526464588d1b9a4d9cfa Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 18 Oct 2024 10:52:55 -0400 Subject: [PATCH 69/79] by default, lock editing of decomposed productions in the IDE --- cls/SourceControl/Git/Extension.cls | 9 ++++++--- cls/SourceControl/Git/Settings.cls | 4 ++++ cls/SourceControl/Git/Utils.cls | 5 +++++ csp/gitprojectsettings.csp | 13 +++++++++++++ docs/production-decomposition.md | 7 ++++++- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 19e7249d..b23c976f 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -408,7 +408,11 @@ ClassMethod FullExternalName(InternalName As %String) As %String Method IsReadOnly(InternalName As %String) As %Boolean { - quit ##class(SourceControl.Git.Utils).Locked() + set settings = ##class(SourceControl.Git.Settings).%New() + quit ##class(SourceControl.Git.Utils).Locked() + || (##class(SourceControl.Git.Utils).ItemIsProductionToDecompose($get(InternalName)) + && 'settings.decomposeProdAllowIDE + && '##class(SourceControl.Git.Production).IsEnsPortal()) } /// Called before the item is saved to the database it is passed @@ -439,7 +443,7 @@ Method OnBeforeSave(InternalName As %String, Location As %String = "", Object As Method GetStatus(ByRef InternalName As %String, ByRef IsInSourceControl As %Boolean, ByRef Editable As %Boolean, ByRef IsCheckedOut As %Boolean, ByRef UserCheckedOut As %String) As %Status { set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(.InternalName) - set Editable='..IsReadOnly(),IsCheckedOut=1,UserCheckedOut="" + set Editable='..IsReadOnly($get(InternalName)),IsCheckedOut=1,UserCheckedOut="" set filename=##class(SourceControl.Git.Utils).FullExternalName(.InternalName) set IsInSourceControl=(filename'=""&&($$$FileExists(filename))) if filename="" quit $$$OK @@ -505,4 +509,3 @@ Method CheckCommitterIdentity(Settings As SourceControl.Git.Settings, ByRef Acti } } - diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 73129775..03fcc7f8 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -26,6 +26,9 @@ Property settingsUIReadOnly As %Boolean [ InitialExpression = {##class(SourceCon /// Interoperability productions are source-controlled under separate files for each configuration item Property decomposeProductions As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).DecomposeProductions()} ]; +/// Allow editing a decomposed production class in an IDE +Property decomposeProdAllowIDE As %Boolean [ InitialExpression = {##class(SourceControl.Git.Utils).DecomposeProdAllowIDE()} ]; + /// Attribution: Git username for user ${username} Property gitUserName As %String(MAXLEN = 255) [ InitialExpression = {##class(SourceControl.Git.Utils).GitUserName()} ]; @@ -156,6 +159,7 @@ Method %Save() As %Status if (..decomposeProductions & ($data(..Mappings("PTD"))<10)) { set ..Mappings("PTD","*") = "ptd/" // configure a default mapping for PTD items } + set @storage@("settings","decomposeProdAllowIDE") = ..decomposeProdAllowIDE kill @##class(SourceControl.Git.Utils).MappingsNode() merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 52c3d6fb..4354020e 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -77,6 +77,11 @@ ClassMethod DecomposeProductions() As %Boolean [ CodeMode = expression ] $Get(@..#Storage@("settings","decomposeProductions"), 0) } +ClassMethod DecomposeProdAllowIDE() As %Boolean [ CodeMode = expression ] +{ +$Get(@..#Storage@("settings","decomposeProdAllowIDE"), 0) +} + /// Returns the current (or previous) value of the flag. ClassMethod Locked(newFlagValue As %Boolean) As %Boolean { diff --git a/csp/gitprojectsettings.csp b/csp/gitprojectsettings.csp index a758bb62..f5f87c3c 100644 --- a/csp/gitprojectsettings.csp +++ b/csp/gitprojectsettings.csp @@ -96,6 +96,7 @@ body { set settings.compileOnImport = ($Get(%request.Data("compileOnImport", 1)) = 1) set settings.decomposeProductions = ($Get(%request.Data("decomposeProductions", 1)) = 1) + set settings.decomposeProdAllowIDE = ($Get(%request.Data("decomposeProdAllowIDE", 1)) = 1) if ($Get(%request.Data("basicMode", 1)) = 1) { set settings.basicMode = 1 @@ -384,6 +385,18 @@ body {
+
+ +
+
+ + +
+
+
diff --git a/docs/production-decomposition.md b/docs/production-decomposition.md index 14d03df4..2ba850b6 100644 --- a/docs/production-decomposition.md +++ b/docs/production-decomposition.md @@ -6,7 +6,12 @@ The feature may be enabled by checking the "Decompose Productions" box in the Gi If there are existing productions in the namespace, they should be migrated to the new decomposed format by running `do ##class(SourceControl.Git.API).BaselineProductions()`. You may then use the Git Web UI to view, commit, and push the corresponding changes. This method should be run in a single namespace and then deployed to other namespaces through normal git-source-control deployment mechanisms. +## Editing productions in the IDE +There are a couple of limitations related to editing a production class directly in an integrated development environment (Studio or VS Code). +- Any elements of the class definition other than the production definition (for example, methods, parameters, or a custom superclass) are not source controlled if production decomposition is enabled. A recommended workaround is to move these items to a separate utility class. +- The hooks in the IDE are not able to detect which specific production items are being edited. As a result, if any item has an uncommitted change from a different user, you will be blocked from editing the production in the IDE entirely. +As a result of these limitations, editing decomposed productions in the IDE is prohibited by default. To enable it, enable the "Decomposed Productions Allow IDE" setting on the settings page. + ## Known Limitations -- The source control hooks for Production Decomposition are currently only supported when editing via the Interoperability Portal. Editing the production class directly in VS Code or Studio may overwrite other users' changes. - Any custom methods, parameters, etc. in the production class will not be source controlled if Production Decomposition is enabled. A recommended workaround is to move these items to a separate utility class. - Production Decomposition is not supported for deployment of changes to productions using the InterSystems Package Manager. From b77b0257069440d6c46af79a24689255f4c999fa Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:17:18 -0400 Subject: [PATCH 70/79] fix issue detecting production edits from VS Code --- cls/SourceControl/Git/Production.cls | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 1d77be28..aee8473a 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -329,7 +329,8 @@ ClassMethod GetModifiedItemsAfterSave(internalName, Output modifiedItems) /// Check if current CSP session is EnsPortal page ClassMethod IsEnsPortal() As %Boolean { - Return $Data(%request) && '($IsObject(%request) && (%request.UserAgent [ "Code")) + Return $Data(%request) && '($IsObject(%request) && + ((%request.UserAgent [ "Code") || (%request.UserAgent [ "node-fetch"))) } /// Perform check if Production Decomposition logic should be used for given item From 83ffd8e16d0080bb0da587b141e997fcfb4d0895 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:28:48 -0400 Subject: [PATCH 71/79] update changelog for production decomposition --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d10f0375..b06e928c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added 'git push --force' in expert mode (#527) - Add remote repository to settings page (#448) - Added environment awareness in configuration, and showing of environment name in UI (#124) +- Production Decomposition mode allows controlling interoperability productions as individual files for each host (#469) ### Fixed - Fixed display of other users' username in workspace view on Unix (#530) From ed19c80623c2c81f419831253d888556ad723435 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Wed, 30 Oct 2024 16:34:46 -0400 Subject: [PATCH 72/79] fix: PTD items should not be compiled --- cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls index ab641065..15d1e97f 100644 --- a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -24,7 +24,9 @@ Method OnPull() As %Status write !, "WARNING: Deletion of ", ..ModifiedFiles(i).externalName, " failed." } } else { - set compilelist(internalName) = "" + if (##class(SourceControl.Git.Utils).Type(internalName) '= "ptd") { + set compilelist(internalName) = "" + } set nFiles = nFiles + 1 set sc = $$$ADDSC(sc,##class(SourceControl.Git.Utils).ImportItem(internalName, 1)) } From bd27d4a53c58bbc7c6b7868a64ca8dc90f2cfb48 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Thu, 31 Oct 2024 17:02:23 -0400 Subject: [PATCH 73/79] fix: remove duplicate success message on settings save --- csp/gitprojectsettings.csp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/csp/gitprojectsettings.csp b/csp/gitprojectsettings.csp index 2a23ef7a..19d5f9cd 100644 --- a/csp/gitprojectsettings.csp +++ b/csp/gitprojectsettings.csp @@ -172,10 +172,11 @@ body { do buffer.BeginCaptureOutput() $$$ThrowOnError(settings.SaveWithSourceControl()) do buffer.EndCaptureOutput(.out) - &html<
-
#(..EscapeHTML(out))#
-
Settings saved.
-
> + if (out '= "") { + &html<
+
#(..EscapeHTML(out))#
+
> + } } catch err { kill buffer do err.Log() From e84bd103af248138ed698146c6bba5d03cbbc3ee Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:06:11 -0500 Subject: [PATCH 74/79] chore: update version string for next version --- CHANGELOG.md | 6 +++++- module.xml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b066fd3..8f32f212 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.8.0] - 2024-11-04 + +### Added +- Production Decomposition mode allows controlling interoperability productions as individual files for each host (#469) + ## [2.7.0] - 2024-11-04 ### Added @@ -19,7 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Intelligent merge conflict auto-resolution works for the common Business Rule case as well (#391) - All git commands run on the server, their output, and any associated sync output, are logged to a table for diagnostic purposes (#454) - Added API method to automatically add proper %ALL mappings for git-source-control (#214) -- Production Decomposition mode allows controlling interoperability productions as individual files for each host (#469) ### Fixed - Fixed display of other users' username in workspace view on Unix (#530) diff --git a/module.xml b/module.xml index 44732e13..21eba717 100644 --- a/module.xml +++ b/module.xml @@ -3,7 +3,7 @@ git-source-control - 2.7.0 + 2.8.0 Server-side source control extension for use of Git on InterSystems platforms git source control studio vscode module From 6305cc4ea002ce356e7109595c39b8eb2607a4b9 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 5 Nov 2024 11:24:21 -0500 Subject: [PATCH 75/79] fix: production items are deployed after all changed classes are compiled --- .../Git/PullEventHandler/IncrementalLoad.cls | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls index cbde8fb6..5281f01d 100644 --- a/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls +++ b/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls @@ -24,11 +24,13 @@ Method OnPull() As %Status write !, "WARNING: Deletion of ", ..ModifiedFiles(i).externalName, " failed." } } else { - if (##class(SourceControl.Git.Utils).Type(internalName) '= "ptd") { + set nFiles = nFiles + 1 + if (##class(SourceControl.Git.Utils).Type(internalName) = "ptd") { + set ptdList(internalName) = "" + } else { set compilelist(internalName) = "" + set sc = $$$ADDSC(sc,##class(SourceControl.Git.Utils).ImportItem(internalName, 1)) } - set nFiles = nFiles + 1 - set sc = $$$ADDSC(sc,##class(SourceControl.Git.Utils).ImportItem(internalName, 1)) } } @@ -37,6 +39,12 @@ Method OnPull() As %Status quit $$$OK } set sc = $$$ADDSC(sc,$system.OBJ.CompileList(.compilelist, "ck")) + // after compilation, deploy any PTD items + set key = $order(ptdList("")) + while (key '= "") { + set sc = $$$ADDSC(sc, ##class(SourceControl.Git.Utils).ImportItem(key,1)) + set key = $order(ptdList(key)) + } if $$$comClassDefined("Ens.Director") && ##class(Ens.Director).IsProductionRunning() { write !,"Updating production... " set sc = $$$ADDSC(sc,##class(Ens.Director).UpdateProduction()) From 1f7dd474176f2789acb310c1b1da2c0e2b1e8422 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:22:37 -0500 Subject: [PATCH 76/79] fix: fixes and updates to embedded git config file --- cls/SourceControl/Git/Settings.cls | 8 +++++--- cls/SourceControl/Git/Settings/Document.cls | 6 +++--- docs/production-decomposition.md | 6 +++--- test/UnitTest/SourceControl/Git/Settings.cls | 10 +++++----- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cls/SourceControl/Git/Settings.cls b/cls/SourceControl/Git/Settings.cls index 1c81bd31..94f85d91 100644 --- a/cls/SourceControl/Git/Settings.cls +++ b/cls/SourceControl/Git/Settings.cls @@ -86,11 +86,11 @@ Method SaveWithSourceControl() As %Status set sc = $$$OK #dim %SourceControl As %Studio.SourceControl.Interface if '$isobject($get(%SourceControl)) { + new %SourceControl do ##class(%Studio.SourceControl.Interface).SourceControlCreate($username) } set gitDir = ##class(%File).NormalizeDirectory(..namespaceTemp)_".git" - set skipSourceControl = ('##class(%File).DirectoryExists(gitDir) && - (##class(%Studio.SourceControl.Interface).SourceControlClassGet() = ##class(SourceControl.Git.Extension).%ClassName(1))) + set skipSourceControl = '##class(%File).DirectoryExists(gitDir) $$$QuitOnError(..%Save()) set internalName = ##class(SourceControl.Git.Settings.Document).#INTERNALNAME set settingsDoc = ##class(SourceControl.Git.Settings.Document).%New(internalName) @@ -104,7 +104,7 @@ Method SaveWithSourceControl() As %Status $$$QuitOnError(sc) } } - if ($IsObject(%SourceControl)) { + if ($IsObject($get(%SourceControl))) { $$$QuitOnError(%SourceControl.OnAfterSave(internalName)) } } @@ -223,6 +223,8 @@ Method ImportDynamicObject(pSettingsDyn As %DynamicObject) set ..Mappings(k1, k2) = v2.%Get("directory") if v2.%Get("noFolders") { set ..Mappings(k1, k2, "NoFolders") = 1 + } else { + kill ..Mappings(k1, k2, "NoFolders") } } } diff --git a/cls/SourceControl/Git/Settings/Document.cls b/cls/SourceControl/Git/Settings/Document.cls index fe93a3fc..896be064 100644 --- a/cls/SourceControl/Git/Settings/Document.cls +++ b/cls/SourceControl/Git/Settings/Document.cls @@ -1,12 +1,12 @@ -/// Custom studio document type for git-source-control settings that are controlled by a file +/// Custom studio document type for Embedded Git settings that are controlled by a file Class SourceControl.Git.Settings.Document Extends %Studio.AbstractDocument { Projection RegisterExtension As %Projection.StudioDocument(DocumentExtension = "GSC", DocumentNew = 0, DocumentType = "json"); -Parameter INTERNALNAME = "git-source-control.GSC"; +Parameter INTERNALNAME = "embedded-git-config.GSC"; -Parameter EXTERNALNAME = "git-source-control.json"; +Parameter EXTERNALNAME = "embedded-git-config.json"; /// Return 1 if the routine 'name' exists and 0 if it does not. ClassMethod Exists(name As %String) As %Boolean diff --git a/docs/production-decomposition.md b/docs/production-decomposition.md index 2ba850b6..b951fe84 100644 --- a/docs/production-decomposition.md +++ b/docs/production-decomposition.md @@ -1,10 +1,10 @@ # Production Decomposition -Production Decomposition is a feature of git-source-control that allows multiple developers to edit the same IRIS Interoperability production in the same namespace. In the past, the production class has been an obstacle preventing organizations using multi-user development namespaces from adopting source control. Production Decomposition resolves this by representing the production as a directory of files for each production item that may be edited independently. An uncommitted change to the settings for a single item through the Interoperability Portal will block other users from editing that item while allowing changes to other items in the production. +Production Decomposition is a feature of Embedded Git that allows multiple developers to edit the same IRIS Interoperability production in the same namespace. In the past, the production class has been an obstacle preventing organizations using multi-user development namespaces from adopting source control. Production Decomposition resolves this by representing the production as a directory of files for each production item that may be edited independently. An uncommitted change to the settings for a single item through the Interoperability Portal will block other users from editing that item while allowing changes to other items in the production. ## Enabling production decomposition -The feature may be enabled by checking the "Decompose Productions" box in the Git Settings page. For deployment of changes to other environments through git to work properly, the value of this setting must match on all namespaces connected to this repository. To assist, settings are automatically exported into a `git-source-control.json` file at the root of the repository that may be committed and imported into other environments. +The feature may be enabled by checking the "Decompose Productions" box in the Git Settings page. For deployment of changes to other environments through git to work properly, the value of this setting must match on all namespaces connected to this repository. To assist, settings are automatically exported into a `embedded-git-config.json` file at the root of the repository that may be committed and imported into other environments. -If there are existing productions in the namespace, they should be migrated to the new decomposed format by running `do ##class(SourceControl.Git.API).BaselineProductions()`. You may then use the Git Web UI to view, commit, and push the corresponding changes. This method should be run in a single namespace and then deployed to other namespaces through normal git-source-control deployment mechanisms. +If there are existing productions in the namespace, they should be migrated to the new decomposed format by running `do ##class(SourceControl.Git.API).BaselineProductions()`. You may then use the Git Web UI to view, commit, and push the corresponding changes. This method should be run in a single namespace and then deployed to other namespaces through normal Embedded Git deployment mechanisms. ## Editing productions in the IDE There are a couple of limitations related to editing a production class directly in an integrated development environment (Studio or VS Code). diff --git a/test/UnitTest/SourceControl/Git/Settings.cls b/test/UnitTest/SourceControl/Git/Settings.cls index 3334a3b9..d2ebb34a 100644 --- a/test/UnitTest/SourceControl/Git/Settings.cls +++ b/test/UnitTest/SourceControl/Git/Settings.cls @@ -67,12 +67,12 @@ Method TestSaveAndImportSettings() set settings.percentClassReplace = "_" set settings.decomposeProductions = 1 $$$ThrowOnError(settings.SaveWithSourceControl()) - do $$$AssertStatusOK(##class(SourceControl.Git.Utils).AddToSourceControl("git-source-control.GSC")) + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).AddToSourceControl("embedded-git-config.GSC")) // settings file should be in source control - do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("git-source-control.GSC")) - do $$$AssertEquals($replace(##class(SourceControl.Git.Utils).ExternalName("git-source-control.GSC"),"\","/"),"git-source-control.json") + do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("embedded-git-config.GSC")) + do $$$AssertEquals($replace(##class(SourceControl.Git.Utils).ExternalName("embedded-git-config.GSC"),"\","/"),"embedded-git-config.json") // commit settings - do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Commit("git-source-control.GSC")) + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Commit("embedded-git-config.GSC")) // settings should be in the global do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo/") do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.Default") @@ -90,7 +90,7 @@ Method TestSaveAndImportSettings() do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"x") do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"0") // revert change to settings - do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Revert("git-source-control.GSC")) + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Revert("embedded-git-config.GSC")) // old setting should be in the global do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo/") do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.Default") From 391fd889f1429d76d914ffe8327684dbdd598549 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 12 Nov 2024 14:43:36 -0500 Subject: [PATCH 77/79] port production class changes from %Studio.SourceControl --- cls/SourceControl/Git/Production.cls | 57 +++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index aee8473a..56a56c70 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -1,9 +1,15 @@ -/// This class serves as an intermediary for managing Source Control for Productions. -/// Included is handling for exporting Productions as individual pieces of settings -/// and importing individual item settings. +/// This is a replica of %Studio.SourceControl.Production for backwards compatibility with older IRIS versions. Class SourceControl.Git.Production Extends %RegisteredObject { +/// This Parameter should be updated when synced from Perforce +Parameter SrcVer = "$Id: //custom_ccrs/_common/config/cls/Studio/SourceControl/Production.xml#3 $"; + +/// Revision number of this class when compiled as part of the //custom_ccrs/_common/config/... branch. +/// This version will not be updated (by design) when the class is integrated to other branches. +/// This allows the user to tell what version of the Studio client tools are in use. +Parameter Version = 4; + /// Exports settings for a given Production and each Config Item from /// the ProductionDefinition as separate XMLs. These are exported to /// the appropriate file based on nameMethod of the source control class @@ -159,9 +165,16 @@ ClassMethod ExportPTD(internalName As %String, nameMethod) As %Status /// Imports a PTD into a produciton given an external name and produciton name ClassMethod ImportPTD(externalName As %String, productionName As %String) As %Status { - set rollbackFile = ##class(%File).TempFilename() - set sc = ##class(Ens.Deployment.Deploy).DeployCode(externalName,productionName,0,rollbackFile) - do ##class(%File).Delete(rollbackFile) + try { + set ^IRIS.Temp("sscProd",$job,"bypassLock") = 1 + set rollbackFile = ##class(%File).TempFilename() + set sc = ##class(Ens.Deployment.Deploy).DeployCode(externalName,productionName,0,rollbackFile) + do ##class(%File).Delete(rollbackFile) + kill ^IRIS.Temp("sscProd",$job,"bypassLock") + } catch err { + kill ^IRIS.Temp("sscProd",$job,"bypassLock") + set sc = err.AsStatus() + } return sc } @@ -171,7 +184,7 @@ ClassMethod ImportPTDsDir(directory As %String, isDecompMethod As %String = "") set sc = $$$OK set rs = ##class(%ResultSet).%New("%File:FileSet") $$$ThrowOnError(rs.Execute(directory, "*.xml", "", 1)) - $$$ThrowSQLIfError(rs.%SQLCODE, rs.%Message) + throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE, rs.%Message) while rs.Next() { set path = rs.Data("Name") set type = rs.Data("Type") @@ -347,13 +360,16 @@ ClassMethod IsProductionClass(className As %String, nameMethod As %String) As %B } // check if there is a class export for this Production, load it for class definition set filename = $classmethod(##class(%Studio.SourceControl.Interface).SourceControlClassGet(), nameMethod, className_".CLS") - if ##class(%File).Exists(filename) { + if ##class(%File).Exists(filename) && (##class(%File).GetFileSize(filename) '= 0) { $$$ThrowOnError($System.OBJ.Load(filename)) } // if Production exists as a class definition on the server, check if extending Ens.Production set classDef = ##class(%Dictionary.ClassDefinition).%OpenId(className) - if $isobject(classDef) && ##class(%Dictionary.ClassDefinition).%ExistsId(classDef.Super) { - return $classmethod(classDef.Super, "%Extends", "Ens.Production") + if $isobject(classDef) { + for i=1:1:classDef.XDatas.Count() { + set xdata = classDef.XDatas.GetAt(i) + if xdata.Name = "ProductionDefinition" return 1 + } } } return 0 @@ -505,6 +521,7 @@ ClassMethod GetUserProductionChanges(productionName As %String, ByRef items) /// Executes a SQL query without privilege checking if possible on this IRIS version ClassMethod ExecDirectNoPriv(sql, args...) As %SQL.StatementResult { + // once minimum version is IRIS 2021.1.3, remove and just use %ExecDirectNoPriv try { set rs = ##class(%SQL.Statement).%ExecDirectNoPriv(,sql,args...) } catch err { @@ -513,4 +530,24 @@ ClassMethod ExecDirectNoPriv(sql, args...) As %SQL.StatementResult return rs } +/// Handles all compile-time logic for this class +ClassMethod OnToolsCompile() [ CodeMode = objectgenerator, Internal ] +{ + For i = 1:1:%class.Parameters.Count() { + If %class.Parameters.GetAt(i).Name = "SrcVer" Set valSrcVer = %class.Parameters.GetAt(i).Default + If %class.Parameters.GetAt(i).Name = "Version" Set indexVersion=i + } + If $D(indexVersion)&&($G(valSrcVer)["//custom_ccrs/_common/config/") { + Set %class.Parameters.GetAt(indexVersion).Default = $P($G(valSrcVer),"#",2)+1 + Do %class.Parameters.GetAt(indexVersion).%Save() + } + Quit $$$OK +} + +/// Returns value the Major.Minor version for this instance, so it can be used in comparison code which makes sure certain features are used in appropriate versions +ClassMethod InstanceVersion() As %Numeric [ CodeMode = expression ] +{ +$P($SYSTEM.Version.GetNumber(),".",1,2) +} + } From ad30a5e266191d5ebcd7c06aa758da6394df5100 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Tue, 12 Nov 2024 15:04:29 -0500 Subject: [PATCH 78/79] fix: PTD items may be deployed into locked namespace --- cls/SourceControl/Git/Extension.cls | 3 ++- cls/SourceControl/Git/Utils.cls | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index f01b7ad2..d9f4287a 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -430,7 +430,8 @@ ClassMethod FullExternalName(InternalName As %String) As %String Method IsReadOnly(InternalName As %String) As %Boolean { set settings = ##class(SourceControl.Git.Settings).%New() - quit ##class(SourceControl.Git.Utils).Locked() + quit (##class(SourceControl.Git.Utils).Locked() + && '$get(^IRIS.Temp("sscProd",$job,"bypassLock"))) || (##class(SourceControl.Git.Utils).ItemIsProductionToDecompose($get(InternalName)) && 'settings.decomposeProdAllowIDE && '##class(SourceControl.Git.Production).IsEnsPortal()) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 7b535eb3..9b328ebc 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -1400,9 +1400,7 @@ ClassMethod ImportItem(InternalName As %String, force As %Boolean = 0, verbose A set sc = ##class(SourceControl.Git.Production).CreateProduction(targetProduction) } if $$$ISOK(sc) { - set rollbackFile = ##class(%File).TempFilename() - set sc = ##class(Ens.Deployment.Deploy).DeployCode(filename,targetProduction,0,rollbackFile) - do ##class(%File).Delete(rollbackFile) + set sc = ##class(SourceControl.Git.Production).ImportPTD(filename, targetProduction) } } } elseif ..ItemIsProductionToDecompose(InternalName) { From e8e720962b6552eeaae23fc40c412a52d79fd3d7 Mon Sep 17 00:00:00 2001 From: Pravin Barton <9560941+isc-pbarton@users.noreply.github.com> Date: Fri, 15 Nov 2024 10:21:58 -0500 Subject: [PATCH 79/79] chore: remove srcver ported from client tools --- cls/SourceControl/Git/Production.cls | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/cls/SourceControl/Git/Production.cls b/cls/SourceControl/Git/Production.cls index 56a56c70..1ffbe553 100644 --- a/cls/SourceControl/Git/Production.cls +++ b/cls/SourceControl/Git/Production.cls @@ -2,14 +2,6 @@ Class SourceControl.Git.Production Extends %RegisteredObject { -/// This Parameter should be updated when synced from Perforce -Parameter SrcVer = "$Id: //custom_ccrs/_common/config/cls/Studio/SourceControl/Production.xml#3 $"; - -/// Revision number of this class when compiled as part of the //custom_ccrs/_common/config/... branch. -/// This version will not be updated (by design) when the class is integrated to other branches. -/// This allows the user to tell what version of the Studio client tools are in use. -Parameter Version = 4; - /// Exports settings for a given Production and each Config Item from /// the ProductionDefinition as separate XMLs. These are exported to /// the appropriate file based on nameMethod of the source control class @@ -530,20 +522,6 @@ ClassMethod ExecDirectNoPriv(sql, args...) As %SQL.StatementResult return rs } -/// Handles all compile-time logic for this class -ClassMethod OnToolsCompile() [ CodeMode = objectgenerator, Internal ] -{ - For i = 1:1:%class.Parameters.Count() { - If %class.Parameters.GetAt(i).Name = "SrcVer" Set valSrcVer = %class.Parameters.GetAt(i).Default - If %class.Parameters.GetAt(i).Name = "Version" Set indexVersion=i - } - If $D(indexVersion)&&($G(valSrcVer)["//custom_ccrs/_common/config/") { - Set %class.Parameters.GetAt(indexVersion).Default = $P($G(valSrcVer),"#",2)+1 - Do %class.Parameters.GetAt(indexVersion).%Save() - } - Quit $$$OK -} - /// Returns value the Major.Minor version for this instance, so it can be used in comparison code which makes sure certain features are used in appropriate versions ClassMethod InstanceVersion() As %Numeric [ CodeMode = expression ] {