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 {
+