diff --git a/CHANGELOG.md b/CHANGELOG.md index f531668d..480b2550 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Import All no longer deletes items that are mapped in from another database, which could delete Embedded Git itself where it is mapped instance-wide (#997) +- A custom pull event handler stored in the repository is now loaded and compiled before it runs, so operations on a freshly built instance no longer fail with `` (#1000) ## [2.17.1] - 2026-08-18 diff --git a/cls/SourceControl/Git/PullEventHandler.cls b/cls/SourceControl/Git/PullEventHandler.cls index 5efd2c0e..104db0ae 100644 --- a/cls/SourceControl/Git/PullEventHandler.cls +++ b/cls/SourceControl/Git/PullEventHandler.cls @@ -1,3 +1,5 @@ +Include (%occStatus, %occErrors) + /// Base class for all event handlers for git pull commands. /// Subclasses may override to perform an incremental load/compile, take no action, do a zpm "load", etc. Class SourceControl.Git.PullEventHandler Extends %RegisteredObject @@ -28,16 +30,17 @@ ClassMethod ForModifications(ByRef files, pullEventClass As %String) As %Status #Dim pullError As %Exception.AbstractException #Dim st As %Status = $$$OK try { + set handlerClass = $select( + $data(pullEventClass)#2: pullEventClass, + 1: ##class(SourceControl.Git.Utils).PullEventClass()) + set st = ..BootstrapPullEventHandler(handlerClass) + quit:$$$ISERR(st) set log = ##class(SourceControl.Git.DeploymentLog).%New() set log.HeadRevision = ##class(SourceControl.Git.Utils).GetCurrentRevision() set log.StartTimestamp = $zdatetime($ztimestamp,3) set st = log.%Save() quit:$$$ISERR(st) - set event = $classmethod( - $select( - $data(pullEventClass)#2: pullEventClass, - 1: ##class(SourceControl.Git.Utils).PullEventClass()) - ,"%New") + set event = $classmethod(handlerClass,"%New") set event.LocalRoot = ##class(SourceControl.Git.Utils).TempFolder() merge event.ModifiedFiles = files try { @@ -56,6 +59,36 @@ ClassMethod ForModifications(ByRef files, pullEventClass As %String) As %Status quit st } +/// Makes sure class is compiled and can be instantiated, loading it from the +/// local repository if needed. +ClassMethod BootstrapPullEventHandler(class As %String) As %Status [ Private ] +{ + quit:$$$comClassDefined(class) $$$OK + + set internalName = class_".cls" + if ##class(%Dictionary.ClassDefinition).%ExistsId(class) { + write !, "Compiling pull event handler ", class + set source = "compiled from the definition already in the database" + set sc = $system.OBJ.Compile(class, "ck") + } else { + // nothing has been imported yet, so the handler is compiled on its own here: + // a handler kept in the repository it loads must be able to compile by itself. + set filename = ##class(SourceControl.Git.Utils).FullExternalName(internalName) + if '##class(%File).Exists(filename) { + quit $$$ERROR($$$GeneralError, "Pull event class '"_class_"' is not compiled and no source file exists at "_filename) + } + write !, "Importing pull event handler ", class + set source = "imported from "_filename + set sc = ##class(SourceControl.Git.Utils).ImportItem(internalName, 1, 0, 1) + } + quit:$$$ISERR(sc) sc + + if '$$$comClassDefined(class) { + quit $$$ERROR($$$GeneralError, "Pull event class '"_class_"' was "_source_" but is still not available") + } + quit $$$OK +} + /// InternalName may be a comma-delimited string or $ListBuild list ClassMethod ForInternalNames(InternalName As %String) As %Status { diff --git a/test/UnitTest/SourceControl/Git/PullEventHandler/CustomHandler.cls b/test/UnitTest/SourceControl/Git/PullEventHandler/CustomHandler.cls new file mode 100644 index 00000000..4b2517d4 --- /dev/null +++ b/test/UnitTest/SourceControl/Git/PullEventHandler/CustomHandler.cls @@ -0,0 +1,99 @@ +/// Tests that a pull event handler living in the repository itself can be used +/// on an instance where it has not been loaded/compiled yet - see issue #1000. +Class UnitTest.SourceControl.Git.PullEventHandler.CustomHandler Extends UnitTest.SourceControl.Git.AbstractTest +{ + +/// Pull event handler class defined only in the working tree at the start of each test +Parameter FixtureClass As STRING = "TestGit.PullHandlerFixture"; + +/// Global the fixture's OnPull() increments, to prove it actually ran +Parameter MarkerGlobal As STRING = "UnitTestPullHandlerFixture"; + +Method TestHandlerNotInDatabase() +{ + do ..UseFixtureHandler(1) + set sc = ##class(SourceControl.Git.Utils).ImportAll(1) + do $$$AssertStatusOK(sc, "ImportAll succeeds with a pull event handler that is only in the repository") + do $$$AssertTrue($$$comClassDefined(..#FixtureClass), "pull event handler was compiled") + do $$$AssertEquals($get(@..MarkerRef()), 1, "pull event handler's OnPull() ran once") +} + +Method TestHandlerLoadedButNotCompiled() +{ + do ..UseFixtureHandler(0) + set sc = ##class(SourceControl.Git.Utils).ImportAll(1) + do $$$AssertStatusOK(sc, "ImportAll succeeds when items are loaded without being compiled") + do $$$AssertTrue($$$comClassDefined(..#FixtureClass), "pull event handler was compiled") + do $$$AssertEquals($get(@..MarkerRef()), 1, "pull event handler's OnPull() ran once") +} + +Method TestUnavailableHandlerReportsError() +{ + do ..UseFixtureHandler(1) + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.pullEventClass = "TestGit.NoSuchPullHandler" + $$$ThrowOnError(settings.%Save()) + + set sc = ##class(SourceControl.Git.Utils).ImportAll(1) + set errorText = $system.Status.GetErrorText(sc) + do $$$AssertNotTrue($$$ISOK(sc), "ImportAll reports an error when the pull event handler cannot be loaded") + do $$$AssertTrue($find(errorText, "TestGit.NoSuchPullHandler") > 0, "error names the unavailable pull event handler") + do $$$AssertTrue($find(errorText, ##class(SourceControl.Git.Utils).FullExternalName("TestGit.NoSuchPullHandler.cls")) > 0, "error names the file that was looked for") + do $$$AssertNotTrue($find(errorText, "") > 0, "error is not a raw ObjectScript error") +} + +/// Writes the fixture handler to the working tree, configures it as the pull event +/// handler, and removes it from the database so it must be bootstrapped on import. +Method UseFixtureHandler(compileOnImport As %Boolean) +{ + set settings = ##class(SourceControl.Git.Settings).%New() + set settings.pullEventClass = ..#FixtureClass + set settings.compileOnImport = compileOnImport + $$$ThrowOnError(settings.%Save()) + + do ..WriteFile(##class(SourceControl.Git.Utils).FullExternalName(..#FixtureClass_".cls"), ..FixtureSource()) + do ..RemoveFixtureClass() + kill @..MarkerRef() +} + +ClassMethod MarkerRef() As %String [ CodeMode = expression ] +{ +"^"_..#MarkerGlobal +} + +ClassMethod RemoveFixtureClass() +{ + if ##class(%Dictionary.ClassDefinition).%ExistsId(..#FixtureClass) || $$$comClassDefined(..#FixtureClass) { + $$$ThrowOnError($system.OBJ.Delete(..#FixtureClass, "-d")) + } + do ##class(SourceControl.Git.Utils).RemoveRoutineTSH(..#FixtureClass_".cls") +} + +ClassMethod FixtureSource() As %String +{ + set lf = $char(10) + quit "Class "_..#FixtureClass_" Extends SourceControl.Git.PullEventHandler.IncrementalLoad"_lf_ + "{"_lf_ + lf_ + "Parameter NAME = ""Unit Test Fixture"";"_lf_ + lf_ + "Parameter DESCRIPTION = ""Pull event handler used by unit tests."";"_lf_ + lf_ + "Method OnPull() As %Status"_lf_ + "{"_lf_ + " set sc = ##super()"_lf_ + " set ^"_..#MarkerGlobal_" = $get(^"_..#MarkerGlobal_") + 1"_lf_ + " quit sc"_lf_ + "}"_lf_ + lf_ + "}"_lf +} + +Method %OnClose() As %Status [ Private, ServerOnly = 1 ] +{ + do ..RemoveFixtureClass() + kill @..MarkerRef() + quit ##super() +} + +}