Skip to content
Draft
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode/
.gitattributes
*.code-workspace
*.code-workspace
.env
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ 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).

## [Unreleased]

### Changed
- `SourceControl.Git.Change` no longer extends `%Studio.SourceControl.Change`, which is being removed from IRIS Product in 2026.3 and shipped only with CCR (#989). Existing change data is unaffected; no upgrade action is required.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

way too wordy. we don't need to document changes to method definitions unless they are in SourceControl.Git.API. (someday we should prob mark everything else as internal or private or whatev it is). also: don't need to mention version cutoff

- `SourceControl.Git.Change:SetUncommitted` no longer accepts the `EnforceSourcesPath` and `Bulk` arguments, `UpdateUncommitted` no longer accepts `Bulk`, and `ListUncommitted` no longer accepts `RefreshUncommitted`. All were unused by Embedded Git.
- Embedded Git's queries for uncommitted changes (`ListUncommitted`, `GetUserProductionChanges`) now target `SourceControl_Git.Change` directly instead of the shared `%Studio_SourceControl.Change` table. Today the two classes still share the same underlying storage globals, so this does not change what data is visible to any consumer; it removes Embedded Git's dependency on the product-owned table ahead of its removal (#989).

## [2.17.1] - 2026-08-18

### Fixed
Expand Down
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,11 @@ This spins up a single container:
#### Important Notes

- The repository is mounted at `/home/irisowner/dev/git-source-control/` inside the container.
- If port 52774 is already in use, edit the port mapping in `docker-compose.yml`.
- If port 52774 is already in use, create a `.env` file in the root directory setting `IRIS_WEB_PORT` to a free port.
```bash
echo IRIS_WEB_PORT=52775 > .env
docker compose up -d --build
```
- If you have an InterSystems license key at `~/iris.key`, it will be mounted into the container automatically.

#### Development
Expand Down
187 changes: 176 additions & 11 deletions cls/SourceControl/Git/Change.cls
Original file line number Diff line number Diff line change
@@ -1,14 +1,55 @@
Include SourceControl.Git

Class SourceControl.Git.Change Extends (%Persistent, %Studio.SourceControl.Change)
Class SourceControl.Git.Change Extends %Persistent
{

/// Returns the name of the backup file for this Item in the file system
/// Unused in this class, so override to avoid errors.
Property BackupName As %String [ Calculated, SqlComputeCode = {s {*} = ""}, SqlComputed ];
/// Each namespace has its own change extent, and these globals are shared with the
/// Studio SourceControl product's Change class where that class is installed, so the
/// Extent Manager must not register them for this class.
Parameter MANAGEDEXTENT As INTEGER [ Constraint = "0,1", Flags = ENUM ] = 0;

/// Returns the name of this Item in the file system
Property ExternalName As %String [ Calculated, SqlComputeCode = {S {*} = "" }, SqlComputed ];
/// Data global for this class, used by BuildNewIndexes(). Must match the Storage definition below.
Parameter DataGlobal = "^Studio.SourceControl.ChangeD";

/// Index global for this class, used by BuildNewIndexes(). Must match the Storage definition below.
Parameter IndexGlobal = "^Studio.SourceControl.ChangeI";

/// Action for this change
Property Action As %String(VALUELIST = ",add,edit,predelete,delete,revert") [ Required ];

/// Flag indicating whether this change was committed through the source control tools.
/// A committed change with ActivelyCommitted = 0 may have been reverted or committed outside of IRIS.
Property ActivelyCommitted As %Boolean [ InitialExpression = 0 ];

/// CCR associated with this change when it was created (optional)
Property CCR As %String;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

take the opportunity to prune fields we don't need, like CCR


/// User who made this change. For uncommitted changes, this is the user who has it checked out.
Property ChangedBy As %String;

/// Flag indicating whether this change has been committed
Property Committed As %Boolean [ InitialExpression = 0, Required ];

/// Time when this change was committed, in $zdt($H,3) format
Property CommittedTime As %TimeStamp [ InitialExpression = {$zdt("0,0",3)} ];

/// Name used within the instance to refer to this changed item
Property InternalName As %String(MAXLEN = 200);

/// Location where this item is stored on disk
Property ItemFile As %String(MAXLEN = 500) [ Required ];

/// Human readable name for this changed item. Defaults to InternalName if not set explicitly.
Property Name As %String(MAXLEN = 200);

/// Time at which the change was marked as issued to source control
Property P4Issued As %TimeStamp;

/// Utility by which this change was made
Property Source As %String(VALUELIST = ",studio,terminal,trakcare,healthshare,tier2") [ InitialExpression = "studio" ];

/// Last time this change was saved locally
Property UpdatedTime As %TimeStamp [ InitialExpression = {$zdt($H,3)} ];

Index CommittedMap On Committed [ Type = bitmap ];

Expand All @@ -26,8 +67,8 @@ Index ChangeList On (ItemFile, CommittedTime) [ Data = Action, Unique ];
/// <li><var>FileList</var> - the name of the file to revert, or an array subscripted by filenames (e.g. <code>FileList("C:\Perforce\custom_ccrs\us\ISCX\TESTSYS\cls\User\Test.xml")=""</code>)</li>
/// <li><var>Display</var> - boolean flag controlling whether to display the results to standard output (defaults to '1')</li>
/// <li><var>Revert</var> - boolean flag controlling whether to revert a checkout which would delete the change from the change history table (defaults to '0')</li>
/// <li><var>ActiveCommit</var> - used for historical tracking purposes for the %Studio_SourceControl.Change table</li>
/// <li><var>CommitCCR</var> - used for historical tracking purposes for the %Studio_SourceControl.Change table</li>
/// <li><var>ActiveCommit</var> - used for historical tracking purposes when Committed changes are kept for historical reference</li>
/// <li><var>CommitCCR</var> - used for historical tracking purposes when Committed changes are kept for historical reference</li>
/// </ul>
ClassMethod RemoveUncommitted(FileList, Display = 1, Revert = 0, ActiveCommit = 1, CommitCCR) As %Status
{
Expand Down Expand Up @@ -63,7 +104,7 @@ ClassMethod RemoveUncommitted(FileList, Display = 1, Revert = 0, ActiveCommit =
ClassMethod AddDeletedToUncommitted(Filename, InternalName) As %Status
{

Quit ..SetUncommitted(Filename, "delete", InternalName, $USERNAME, "", 1, "", "", 0)
Quit ..SetUncommitted(Filename, "delete", InternalName, $USERNAME, "", 1, "", "")
}

/// Determine if an item is deleted, if it is not in a provided list of git files but is a known tracked item
Expand Down Expand Up @@ -128,7 +169,7 @@ ClassMethod RefreshUncommitted(Display = 0, IncludeRevert = 0, Output gitFiles,
kill gitFiles

// files from the uncommitted queue
set sc=..ListUncommitted(.tFileList,IncludeRevert,0)
set sc=..ListUncommitted(.tFileList,IncludeRevert)
if $$$ISERR(sc) quit sc

// files from git status
Expand Down Expand Up @@ -180,7 +221,7 @@ ClassMethod RefreshUncommitted(Display = 0, IncludeRevert = 0, Output gitFiles,
1:"add")

if ((##class(%File).Exists(ExternalName)) && ('$ISVALIDNUM(InternalName)) && ('..IsUncommitted(ExternalName)) && ($data($$$TrackedItems(InternalName)))) {
set sc=..SetUncommitted(ExternalName, action, InternalName, $USERNAME, "", 1, "", "", 0)
set sc=..SetUncommitted(ExternalName, action, InternalName, $USERNAME, "", 1, "", "")
if $$$ISERR(sc) continue
}
}
Expand All @@ -203,6 +244,130 @@ ClassMethod RefreshUncommitted(Display = 0, IncludeRevert = 0, Output gitFiles,
quit sc
}

/// Returns the uncommitted change object for <var>Filename</var>, or "" if it is not in the queue

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i want to be further convinced that these methods were directly copied from %Studio.SourceControl.Change rather than hallucinated. write a utility that copy/pastes them from %Studio source, then diff. or merge from oddDEF like a true ObjectScript geek.

ClassMethod OpenUncommitted(Filename As %String) As SourceControl.Git.Change
{
if '..IsUncommitted(Filename, .id) quit ""
quit ..%OpenId(id)
}

/// Retrieves the details of an item in the uncommitted queue
ClassMethod GetUncommitted(Filename As %String, ByRef Action As %String, ByRef InternalName As %String, ByRef ChangedBy As %String, ByRef Source As %String, ByRef Updated As %TimeStamp, ByRef P4Issued As %TimeStamp, ByRef CCR As %String, ByRef Name As %String) As %Status
{
if '$data(Filename) quit $$$ERROR($$$GeneralError,"'Filename' is a required field")
if '..IsUncommitted(Filename) quit $$$ERROR($$$GeneralError,"Item is not in the Uncommitted queue")
set obj = ..OpenUncommitted(Filename)
set Action = obj.Action
set ChangedBy = obj.ChangedBy
set InternalName = obj.InternalName
set Source = obj.Source
set Updated = obj.UpdatedTime
set P4Issued = obj.P4Issued
set CCR = obj.CCR
set Name = obj.Name
quit $$$OK
}

/// Adds a new item to the uncommitted queue, or updates it if it is already there
ClassMethod SetUncommitted(Filename As %String, Action As %String, InternalName As %String, ChangedBy As %String, Source As %String, P4Issued As %Boolean, CCR As %String, Name As %String) As %Status
{
if '$data(Filename)#2 quit $$$ERROR($$$GeneralError,"'Filename' is a required field")
set Filename = ##class(%File).NormalizeFilename(Filename)
set isCSP = ($extract($get(InternalName)) = "/")
if '(##class(%File).Exists(Filename) || (isCSP && ($get(Action) = "delete"))) {
quit $$$ERROR($$$GeneralError,"File does not exist - "_Filename)
}
if ..IsUncommitted(Filename) {
quit ..UpdateUncommitted(Filename, .Action, .InternalName, .ChangedBy, .Source, , .P4Issued, .CCR, .Name)
}
set obj = ..%New()
set obj.ItemFile = Filename
if $data(Action)#2 set obj.Action = $get(Action)
if $data(ChangedBy)#2 set obj.ChangedBy = $get(ChangedBy)
if $data(InternalName)#2 set obj.InternalName = $get(InternalName)
if $data(Source)#2 set obj.Source = $get(Source)
if $data(CCR)#2 set obj.CCR = $get(CCR)
if $data(Name)#2 set obj.Name = $get(Name)
if $get(P4Issued) set obj.P4Issued = $zdatetime($horolog, 3)
quit obj.%Save()
}

/// Updates the details of an item already in the uncommitted queue
ClassMethod UpdateUncommitted(Filename As %String, Action As %String, InternalName As %String, ChangedBy As %String, Source As %String, Updated, P4Issued As %Boolean, CCR As %String, Name As %String) As %Status
{
if '$data(Filename)#2 quit $$$ERROR($$$GeneralError,"'Filename' is a required field")
set Filename = ##class(%File).NormalizeFilename(Filename)
if '##class(%File).Exists(Filename) && ($get(Action) '= "revert") {
quit $$$ERROR($$$GeneralError,"File does not exist - "_Filename)
}
set obj = ..OpenUncommitted(Filename)
if '$isobject(obj) quit $$$ERROR($$$GeneralError,"'"_Filename_"' is not in an uncommitted change")
if $data(Action)#2 {
if Action '= obj.Action set obj.P4Issued = ""
set obj.Action = $get(Action)
}
if $data(ChangedBy)#2 {
if ChangedBy '= obj.ChangedBy {
quit $$$ERROR($$$GeneralError,"UpdateUncommitted() called on '"_Filename_"' by "_ChangedBy_", which is owned by "_obj.ChangedBy)
}
set obj.ChangedBy = $get(ChangedBy)
set obj.UpdatedTime = $zdatetime($horolog, 3)
}
if $data(InternalName)#2 set obj.InternalName = $get(InternalName)
if $data(Source)#2 set obj.Source = $get(Source)
if $data(CCR)#2 set obj.CCR = $get(CCR)
if $data(Name)#2 set obj.Name = $get(Name)
if $get(P4Issued) set obj.P4Issued = $zdatetime($horolog, 3)
quit obj.%Save()
}

/// Lists the items in the uncommitted queue, subscripted by filename with the action as the value.
/// Items with an action of 'revert' are listed only if <var>IncludeRevert</var> is true.
ClassMethod ListUncommitted(ByRef UncommittedList = "", IncludeRevert As %Boolean = 0) As %Status
{
do ..BuildNewIndexes()
&sql(DECLARE UncommittedCursor CURSOR FOR
SELECT ItemFile, Action INTO :itemFile, :action
FROM SourceControl_Git.Change WHERE Committed = 0)
&sql(OPEN UncommittedCursor)
throw:SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE, %msg)
&sql(FETCH UncommittedCursor)
while SQLCODE = 0 {
if IncludeRevert || (action '= "revert") {
set UncommittedList(itemFile) = action
}
&sql(FETCH UncommittedCursor)
}
&sql(CLOSE UncommittedCursor)
quit $$$OK
}

/// Builds any index defined in this class that has never been built in this namespace.
/// Needed for instances holding change data that predates the addition of an index.
/// The index list must be kept in sync with the Index definitions in this class.
ClassMethod BuildNewIndexes()
{
// nothing to do in a namespace with no change data
if $data(@..#DataGlobal) '= 11 quit
set indexList = ""
for indexName = "ChangeList", "CommittedMap" {
if $data(@..#IndexGlobal@(indexName))\2 = 0 {
set indexList = indexList_$listbuild(indexName)
}
}
if indexList = "" quit
write "Building "_..%ClassName(1)_" indexes in "_$namespace_": "_$listtostring(indexList, ", "),!!
$$$ThrowOnError(..%BuildIndices(indexList, 1, 1))
}

/// Defaults ChangedBy to the current user and Name to InternalName
Method %OnAddToSaveSet(depth As %Integer = 3, insert As %Integer = 0, callcount As %Integer = 0) As %Status [ Private, ServerOnly = 1 ]
{
if ..ChangedBy = "" set ..ChangedBy = $username
if ..Name = "" set ..Name = ..InternalName
quit $$$OK
}

Query InstanceUncommitted() As %Query(ROWSPEC = "InternalName:%String,User:%String,Namespace:%String")
{
}
Expand Down
2 changes: 1 addition & 1 deletion cls/SourceControl/Git/Extension.cls
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ Method OnAfterSave(InternalName As %String, Object As %RegisteredObject = {$$$NU
set forceExport = (InternalName'= "") && ($data(..Modified(InternalName)))
$$$ThrowOnError(##class(SourceControl.Git.Utils).ExportItem(InternalName,,forceExport))
if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", "", 0))
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "edit", InternalName, $username, "", 1, "", ""))
}
}
} else {
Expand Down
2 changes: 1 addition & 1 deletion cls/SourceControl/Git/Production.cls
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ ClassMethod CreateProduction(productionName As %String, superClasses As %String
/// 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 sql = "SELECT InternalName, Action FROM SourceControl_Git.Change WHERE ChangedBy = ? AND Committed = 0 AND InternalName %STARTSWITH ?"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to change SQL permissions documented required? that would probably have to be a major version upgrade.

set rs = ..ExecDirectNoPriv(sql,$username,productionName_"||")
throw:rs.%SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(rs.%SQLCODE,rs.%Message)
while rs.%Next() {
Expand Down
2 changes: 1 addition & 1 deletion cls/SourceControl/Git/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1842,7 +1842,7 @@ ClassMethod ExportItem(InternalName As %String, expand As %Boolean = 1, force As
set filenames($I(filenames)) = filename
$$$QuitOnError(..UpdateRoutineTSH(InternalName, $h))
if '##class(SourceControl.Git.Change).IsUncommitted(filename) {
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "add", InternalName, $username, "", 1, "", "", 0))
$$$ThrowOnError(##class(SourceControl.Git.Change).SetUncommitted(filename, "add", InternalName, $username, "", 1, "", ""))
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ services:
build: .
restart: always
ports:
- 52774:52773
- ${IRIS_WEB_PORT:-52774}:52773
volumes:
- ~/iris.key:/usr/irissys/mgr/iris.key
- ./:/home/irisowner/dev/git-source-control/
Expand Down
5 changes: 4 additions & 1 deletion iriscli
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
# 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}
# Find the container for the compose project at this script's directory.
# Override with IRIS_CONTAINER if needed.
default_project=$(basename "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9_-')
CONTAINER=${IRIS_CONTAINER:-${COMPOSE_PROJECT_NAME:-$default_project}-iris-1}
ARGS=()
PARAMS=()
file=
Expand Down
Loading
Loading