Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<CLASS DOES NOT EXIST>` (#1000)

## [2.17.1] - 2026-08-18

Expand Down
43 changes: 38 additions & 5 deletions cls/SourceControl/Git/PullEventHandler.cls
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -56,6 +59,36 @@ ClassMethod ForModifications(ByRef files, pullEventClass As %String) As %Status
quit st
}

/// Makes sure <var>class</var> 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
}

/// <var>InternalName</var> may be a comma-delimited string or $ListBuild list
ClassMethod ForInternalNames(InternalName As %String) As %Status
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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, "<CLASS DOES NOT EXIST>") > 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()
}

}
Loading