diff --git a/CHANGELOG.md b/CHANGELOG.md index 68cd7d9f..ceb2f881 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.16.1] - Unreleased + +### Fixed +- Changes to % routines mapped to the current namespace may now be added to source control and committed (#944) + ## [2.16.0] - 2026-03-06 ### Added diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index ff7f8f80..2fc98483 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -2414,7 +2414,7 @@ ClassMethod GitStatus(ByRef files, IncludeAllFiles = 0) while $listnext(list, pointer, item) { set operation = $zstrip($extract(item, 1, 2), "<>W") set externalName = $extract(item, 4, *) - set internalName = ..NameToInternalName(externalName,,0) + set internalName = ..NameToInternalName(externalName,0,0) if (internalName '= "") { set files(internalName) = $listbuild(operation, externalName) if (operation '= "D") { @@ -2464,7 +2464,7 @@ ClassMethod Name(InternalName As %String, ByRef MappingExists As %Boolean) As %S set usertype = 0 } - if 'usertype && $$CheckProtect^%qccServer(InternalName) { + if 'usertype && ##class(%Routine).CheckProtect(InternalName) { quit "" } diff --git a/ipm b/ipm new file mode 100644 index 00000000..1510f4ae --- /dev/null +++ b/ipm @@ -0,0 +1,45 @@ +#!/bin/bash + +# ipm: convenience wrapper to run a ZPM command in an IRIS session +# Usage examples: +# ipm git-source-control test -only +# ipm -U USER git-source-control test -only -verbose + +CONTAINER=${IRIS_CONTAINER:-git-source-control-iris-1} +ARGS=() +CMD=() + +# Allow namespace via env +if [ -n "$IRIS_NAMESPACE" ]; then + ARGS+=( -U "$IRIS_NAMESPACE" ) +elif [ -n "$IRISNAMESPACE" ]; then + ARGS+=( -U "$IRISNAMESPACE" ) +fi + +# Optional leading -U +if [ "$1" = "-U" ] && [ -n "$2" ]; then + ARGS+=( -U "$2" ) + shift 2 +fi + +# Remaining args compose the ZPM command string +while [[ $# -gt 0 ]]; do + CMD+=( "$1" ) + shift +done + +if [ ${#CMD[@]} -eq 0 ]; then + echo "Usage: ipm [ -U ] " >&2 + exit 1 +fi + +# Join CMD array into a single string +CMDSTR="${CMD[*]}" +# Escape quotes for ObjectScript string literal +CMDSTR_ESC=${CMDSTR//\"/\"\"} + +# Build an OS snippet and feed to iris session via docker exec +( + echo "zpm \"${CMDSTR_ESC}\":1:1" + echo "halt" +) | docker exec -i "$CONTAINER" iris session iris "${ARGS[@]}" diff --git a/iriscli b/iriscli new file mode 100644 index 00000000..f76cf9e2 --- /dev/null +++ b/iriscli @@ -0,0 +1,41 @@ +#!/bin/bash + +# iriscli: convenience wrapper to run ObjectScript commands or script files in an IRIS session +# Usage examples: +# iriscli -U USER (interactive-like session) +# iriscli -U USER script.txt (run a script file) +# iriscli script.txt arg1 arg2 (run script with parameters) + +CONTAINER=${IRIS_CONTAINER:-git-source-control-iris-1} +ARGS=() +PARAMS=() +file= + +if [ -n "$IRIS_NAMESPACE" ]; then + ARGS+=( -U $IRIS_NAMESPACE ) +elif [ -n "$IRISNAMESPACE" ]; then + ARGS+=( -U $IRISNAMESPACE ) +fi + +while [[ $# -gt 0 ]]; do + if [ -x $1 ]; then + file=$1 + elif [ -z "$file" ]; then + ARGS+=("$1") + else + PARAMS+=("$1") + fi + shift +done + +if [ -n "$file" ]; then + ( + for param in ${PARAMS[@]}; do + echo "Set params(\$i(params)) = \"${param//\"/\"\"}\"" + done + egrep -v '^(;|#|//)|^$' $file; + echo halt + ) | docker exec -i "$CONTAINER" iris session iris "${ARGS[@]}" +else + docker exec -i "$CONTAINER" iris session iris "${ARGS[@]}" +fi diff --git a/module.xml b/module.xml index a04479c8..778a069e 100644 --- a/module.xml +++ b/module.xml @@ -3,7 +3,7 @@ git-source-control - 2.16.0 + 2.16.1 Server-side source control extension for use of Git on InterSystems platforms git source control studio vscode module diff --git a/test/UnitTest/SourceControl/Git/Utils/AddToSourceControl.cls b/test/UnitTest/SourceControl/Git/Utils/AddToSourceControl.cls new file mode 100644 index 00000000..9db70679 --- /dev/null +++ b/test/UnitTest/SourceControl/Git/Utils/AddToSourceControl.cls @@ -0,0 +1,172 @@ +Import SourceControl.Git + +Class UnitTest.SourceControl.Git.Utils.AddToSourceControl Extends UnitTest.SourceControl.Git.AbstractTest +{ + +Property RoutineMappingCreated As %Boolean [ InitialExpression = 0 ]; + +Property PackageMappingCreated As %Boolean [ InitialExpression = 0 ]; + +Method %OnNew(initvalue) As %Status +{ + Set sc = ##super(initvalue) + If $$$ISERR(sc) Quit sc + Set settings = ##class(SourceControl.Git.Settings).%New() + Set settings.Mappings("MAC","*") = "rtn/" + Do settings.%Save() + Quit $$$OK +} + +Method %OnClose() As %Status [ Private, ServerOnly = 1 ] +{ + // Clean up routines + if ##class(%Routine).Exists("%gitunittestroutine.mac") { + do ##class(%Routine).Delete("%gitunittestroutine.mac") + } + if ##class(%Routine).Exists("gitunittestroutine.mac") { + do ##class(%Routine).Delete("gitunittestroutine.mac") + } + // Clean up classes + if $$$defClassDefined("%gitunittest.TestClass") { + do $system.OBJ.Delete("%gitunittest.TestClass.CLS", "-d") + } + if $$$defClassDefined("gitunittest.TestClass") { + do $system.OBJ.Delete("gitunittest.TestClass.CLS", "-d") + } + // Clean up mappings + if ..RoutineMappingCreated { + do ..DeleteRoutineMapping("%gitunittest*") + } + if ..PackageMappingCreated { + do ..DeletePackageMapping("%gitunittest") + } + Quit ##super() +} + +/// Returns the name of the default routine database for this namespace. +Method GetDefaultDBName() As %String +{ + set defaultDB = ##class(%SYS.Namespace).GetRoutineDest($namespace, "gitunittestroutine.mac") + quit ##class(%File).GetDirectoryPiece(defaultDB, ##class(%File).GetDirectoryLength(defaultDB)) +} + +/// Creates a routine mapping in %SYS so that routines matching the given pattern +/// resolve to the current namespace's default routine database. +Method CreateRoutineMapping(pattern As %String) As %Status +{ + set ns = $namespace + set defaultDBName = ..GetDefaultDBName() + + new $namespace + set $namespace = "%SYS" + set props("Database") = defaultDBName + set sc = ##class(Config.MapRoutines).Create(ns, pattern, .props) + quit sc +} + +/// Creates a package mapping in %SYS so that classes in the given package +/// resolve to the current namespace's default routine database. +Method CreatePackageMapping(package As %String) As %Status +{ + set ns = $namespace + set defaultDBName = ..GetDefaultDBName() + + new $namespace + set $namespace = "%SYS" + set props("Database") = defaultDBName + set sc = ##class(Config.MapPackages).Create(ns, package, .props) + quit sc +} + +/// Deletes a routine mapping from %SYS for the given pattern. +ClassMethod DeleteRoutineMapping(pattern As %String) As %Status +{ + set ns = $namespace + new $namespace + set $namespace = "%SYS" + quit ##class(Config.MapRoutines).Delete(ns, pattern) +} + +/// Deletes a package mapping from %SYS for the given package. +ClassMethod DeletePackageMapping(package As %String) As %Status +{ + set ns = $namespace + new $namespace + set $namespace = "%SYS" + quit ##class(Config.MapPackages).Delete(ns, package) +} + +Method TestNonPercentRoutine() +{ + set internalName = "gitunittestroutine.mac" + + set r = ##class(%Routine).%New(internalName) + do r.WriteLine(" write ""hello"",!") + $$$ThrowOnError(r.Save()) + + // Verify FullExternalName returns a non-empty path + set fullName = ##class(Utils).FullExternalName(internalName) + do $$$AssertNotEquals(fullName, "") + + // Add to source control and verify it exported to the file system + do $$$AssertStatusOK(##class(Utils).AddToSourceControl(internalName)) + do $$$AssertTrue(##class(%File).Exists(fullName), "non-% routine file should exist on disk after AddToSourceControl") +} + +Method TestPercentRoutine() +{ + set internalName = "%gitunittestroutine.mac" + + $$$ThrowOnError(..CreateRoutineMapping("%gitunittest*")) + set ..RoutineMappingCreated = 1 + + // Create a routine named %gitunittestroutine.mac + set r = ##class(%Routine).%New(internalName) + do r.WriteLine(" write ""hello"",!") + $$$ThrowOnError(r.Save()) + + + // Verify FullExternalName returns a non-empty path + set fullName = ##class(Utils).FullExternalName(internalName) + do $$$AssertNotEquals(fullName, "") + + // Add to source control and verify it exported to the file system + do $$$AssertStatusOK(##class(Utils).AddToSourceControl(internalName)) + do $$$AssertTrue(##class(%File).Exists(fullName), "% routine file should exist on disk after AddToSourceControl") +} + +Method TestNonPercentClass() +{ + set internalName = "gitunittest.TestClass.CLS" + + set classDef = ##class(%Dictionary.ClassDefinition).%New() + set classDef.Name = "gitunittest.TestClass" + $$$ThrowOnError(classDef.%Save()) + + set fullName = ##class(Utils).FullExternalName(internalName) + do $$$AssertNotEquals(fullName, "", "FullExternalName should return a path for non-% class") + + do $$$AssertStatusOK(##class(Utils).AddToSourceControl(internalName)) + do $$$AssertTrue(##class(%File).Exists(fullName), "non-% class file should exist on disk after AddToSourceControl") +} + +Method TestPercentClass() +{ + set internalName = "%gitunittest.TestClass.CLS" + + $$$ThrowOnError(..CreatePackageMapping("%gitunittest")) + set ..PackageMappingCreated = 1 + + set classDef = ##class(%Dictionary.ClassDefinition).%New() + set classDef.Name = "%gitunittest.TestClass" + $$$ThrowOnError(classDef.%Save()) + $$$ThrowOnError($system.OBJ.Compile("%gitunittest.TestClass", "ck-d")) + + set fullName = ##class(Utils).FullExternalName(internalName) + do $$$AssertNotEquals(fullName, "", "FullExternalName should return a path for % class") + + do $$$AssertStatusOK(##class(Utils).AddToSourceControl(internalName)) + do $$$AssertTrue(##class(%File).Exists(fullName), "% class file should exist on disk after AddToSourceControl") +} + +}