├── test
├── UnitTest
│ ├── coverage.list
│ └── SourceControl
│ │ └── Git
│ │ ├── Extension.cls
│ │ ├── AbstractTest.cls
│ │ ├── Initialization.cls
│ │ ├── Util
│ │ └── Production.cls
│ │ ├── Pull.cls
│ │ ├── BaselineExport.cls
│ │ ├── AddRemove.cls
│ │ ├── ImportAll.cls
│ │ ├── Sync.cls
│ │ ├── NameTest.cls
│ │ └── Settings.cls
└── _resources
│ ├── cls
│ └── UnitTest
│ │ └── SampleProduction.cls
│ ├── ptd
│ └── UnitTest_SampleProduction
│ │ ├── Stgs-c949C.xml
│ │ ├── Stgs-b949C.xml
│ │ └── ProdStgs-UnitTest_SampleProduction.xml
│ └── dfi
│ └── test2.pivot.dfi
├── .gitignore
├── .github
├── CODEOWNERS
└── workflows
│ └── main.yml
├── .gitattributes
├── git-webui
├── .gitignore
├── src
│ └── share
│ │ └── git-webui
│ │ └── webui
│ │ ├── img
│ │ ├── doc
│ │ │ ├── stash.png
│ │ │ ├── workspace.png
│ │ │ ├── commit-explorer.png
│ │ │ ├── commit-history.png
│ │ │ ├── branch-operations.png
│ │ │ └── commit-history-tree-view.png
│ │ ├── git-icon.png
│ │ ├── git-logo.png
│ │ ├── home.svg
│ │ ├── discarded.svg
│ │ ├── context.svg
│ │ ├── inboxes.svg
│ │ ├── gear-fill.svg
│ │ ├── tag.svg
│ │ ├── folder.svg
│ │ ├── computer.svg
│ │ ├── file.svg
│ │ ├── star.svg
│ │ └── branch.svg
│ │ └── js
│ │ └── polyfills.js
├── release
│ └── share
│ │ └── git-webui
│ │ └── webui
│ │ ├── img
│ │ ├── doc
│ │ │ ├── stash.png
│ │ │ ├── workspace.png
│ │ │ ├── commit-explorer.png
│ │ │ ├── commit-history.png
│ │ │ ├── branch-operations.png
│ │ │ └── commit-history-tree-view.png
│ │ ├── git-icon.png
│ │ ├── git-logo.png
│ │ ├── home.svg
│ │ ├── discarded.svg
│ │ ├── context.svg
│ │ ├── inboxes.svg
│ │ ├── gear-fill.svg
│ │ ├── tag.svg
│ │ ├── folder.svg
│ │ ├── computer.svg
│ │ ├── file.svg
│ │ └── star.svg
│ │ └── js
│ │ └── polyfills.js
├── package.json
└── Gruntfile.js
├── docs
├── images
│ ├── hcc
│ │ ├── sync.png
│ │ ├── sidebar.png
│ │ ├── hcc-step-1.png
│ │ ├── hcc-step-3.png
│ │ ├── hcc-step-4.png
│ │ ├── newbranch.png
│ │ ├── pushbranch.png
│ │ ├── commitmessage.png
│ │ ├── mergebranch.png
│ │ ├── newbranchmenu.png
│ │ ├── syncinterface.png
│ │ ├── importallforce.png
│ │ ├── newbranchnaming.png
│ │ ├── configuremappings.png
│ │ ├── developmentsidebar.png
│ │ ├── workspacechanges.png
│ │ ├── configuremergebranch.png
│ │ ├── gitlab_merge_request.png
│ │ └── sync_output_merge_request_link.png
│ ├── settings.PNG
│ ├── basicmodesetting.png
│ ├── source-control-menu.gif
│ └── production-decomposition.png
├── baselining.md
├── scintro.md
├── production-decomposition.md
├── expert.md
├── testing.md
└── menu-items.md
├── cls
├── SourceControl
│ └── Git
│ │ ├── Util
│ │ ├── RuleConflictResolver.cls
│ │ ├── ProductionConflictResolver.cls
│ │ ├── XMLConflictResolver.cls
│ │ └── ResolutionManager.cls
│ │ ├── PullEventHandler
│ │ ├── FullLoad.cls
│ │ ├── PackageManager.cls
│ │ ├── Default.cls
│ │ ├── PackageManagerReload.cls
│ │ └── IncrementalLoad.cls
│ │ ├── Modification.cls
│ │ ├── Build.cls
│ │ ├── StreamServer.cls
│ │ ├── DeploymentLog.cls
│ │ ├── Installer.cls
│ │ ├── PullEventHandler.cls
│ │ ├── PackageManagerContext.cls
│ │ ├── Settings
│ │ └── Document.cls
│ │ ├── Log.cls
│ │ ├── API.cls
│ │ ├── File.cls
│ │ └── DiscardState.cls
└── _zpkg
│ └── isc
│ └── sc
│ └── git
│ ├── SystemMode.cls
│ ├── Defaults.cls
│ ├── Favorites.cls
│ └── Socket.cls
├── inc
└── SourceControl
│ └── Git.inc
├── LICENSE
├── CONTRIBUTING.md
├── csp
├── webuidriver.csp
└── pull.csp
└── module.xml
/test/UnitTest/coverage.list:
--------------------------------------------------------------------------------
1 | SourceControl.Git.PKG
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | .gitattributes
3 | *.code-workspace
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Default Owners
2 | * @isc-pbarton @isc-dchui @isc-tleavitt
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/git-webui/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/
3 | node_modules/
4 | bower_components/
5 |
--------------------------------------------------------------------------------
/docs/images/hcc/sync.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/sync.png
--------------------------------------------------------------------------------
/docs/images/settings.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/settings.PNG
--------------------------------------------------------------------------------
/docs/images/hcc/sidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/sidebar.png
--------------------------------------------------------------------------------
/docs/images/hcc/hcc-step-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/hcc-step-1.png
--------------------------------------------------------------------------------
/docs/images/hcc/hcc-step-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/hcc-step-3.png
--------------------------------------------------------------------------------
/docs/images/hcc/hcc-step-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/hcc-step-4.png
--------------------------------------------------------------------------------
/docs/images/hcc/newbranch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/newbranch.png
--------------------------------------------------------------------------------
/docs/images/hcc/pushbranch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/pushbranch.png
--------------------------------------------------------------------------------
/docs/images/basicmodesetting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/basicmodesetting.png
--------------------------------------------------------------------------------
/docs/images/hcc/commitmessage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/commitmessage.png
--------------------------------------------------------------------------------
/docs/images/hcc/mergebranch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/mergebranch.png
--------------------------------------------------------------------------------
/docs/images/hcc/newbranchmenu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/newbranchmenu.png
--------------------------------------------------------------------------------
/docs/images/hcc/syncinterface.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/syncinterface.png
--------------------------------------------------------------------------------
/docs/images/hcc/importallforce.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/importallforce.png
--------------------------------------------------------------------------------
/docs/images/hcc/newbranchnaming.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/newbranchnaming.png
--------------------------------------------------------------------------------
/docs/images/source-control-menu.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/source-control-menu.gif
--------------------------------------------------------------------------------
/docs/images/hcc/configuremappings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/configuremappings.png
--------------------------------------------------------------------------------
/docs/images/hcc/developmentsidebar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/developmentsidebar.png
--------------------------------------------------------------------------------
/docs/images/hcc/workspacechanges.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/workspacechanges.png
--------------------------------------------------------------------------------
/docs/images/hcc/configuremergebranch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/configuremergebranch.png
--------------------------------------------------------------------------------
/docs/images/hcc/gitlab_merge_request.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/gitlab_merge_request.png
--------------------------------------------------------------------------------
/docs/images/production-decomposition.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/production-decomposition.png
--------------------------------------------------------------------------------
/docs/images/hcc/sync_output_merge_request_link.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/docs/images/hcc/sync_output_merge_request_link.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/doc/stash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/doc/stash.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/git-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/git-icon.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/git-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/git-logo.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/doc/stash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/doc/stash.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/git-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/git-icon.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/git-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/git-logo.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/doc/workspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/doc/workspace.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/doc/workspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/doc/workspace.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/doc/commit-explorer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/doc/commit-explorer.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/doc/commit-history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/doc/commit-history.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/doc/branch-operations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/doc/branch-operations.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/doc/commit-explorer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/doc/commit-explorer.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/doc/commit-history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/doc/commit-history.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/doc/branch-operations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/doc/branch-operations.png
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/doc/commit-history-tree-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/src/share/git-webui/webui/img/doc/commit-history-tree-view.png
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/doc/commit-history-tree-view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/intersystems/git-source-control/HEAD/git-webui/release/share/git-webui/webui/img/doc/commit-history-tree-view.png
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Util/RuleConflictResolver.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.Util.RuleConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver
2 | {
3 |
4 | Parameter ExpectedConflictTag = "";
5 |
6 | }
7 |
8 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Util/ProductionConflictResolver.cls:
--------------------------------------------------------------------------------
1 | Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
2 |
3 | Class SourceControl.Git.Util.ProductionConflictResolver Extends SourceControl.Git.Util.XMLConflictResolver
4 | {
5 |
6 | Parameter ExpectedConflictTag = "";
7 |
8 | Parameter OutputIndent = " ";
9 |
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/inc/SourceControl/Git.inc:
--------------------------------------------------------------------------------
1 | ROUTINE SourceControl.Git [Type=INC]
2 | #define SourceRoot $get(^SYS("SourceControl","Git","settings","namespaceTemp"))
3 | #def1arg SourceMapping(%arg) ^SYS("SourceControl","Git","settings","mappings",%arg)
4 | #def1arg GetSourceMapping(%arg) $Get($$$SourceMapping(%arg))
5 | #def1arg NewLineIfNonEmptyStream(%arg) if %arg.Size > 0 write !
6 | #def1arg TrackedItems(%arg) ^SYS("SourceControl","Git","items", %arg)
7 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PullEventHandler/FullLoad.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.PullEventHandler.FullLoad Extends SourceControl.Git.PullEventHandler
2 | {
3 |
4 | Parameter NAME = "Full Load";
5 |
6 | Parameter DESCRIPTION = "Performs an full load and compile of all items in the repository.";
7 |
8 | Method OnPull() As %Status
9 | {
10 | return ##class(SourceControl.Git.Utils).ImportAll(1,
11 | ##class(SourceControl.Git.PullEventHandler.IncrementalLoad).%ClassName(1))
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Modification.cls:
--------------------------------------------------------------------------------
1 | /// Class to store information about each file that is modified by git pull
2 | Class SourceControl.Git.Modification Extends %RegisteredObject
3 | {
4 |
5 | /// path of the file relative to the Git repository
6 | Property externalName As %String;
7 |
8 | /// Name in IRIS SourceControl.Git.Modification
9 | Property internalName As %String;
10 |
11 | /// Type of change (A|C|D|M|R|T|U|X|B). See git diff documentation.
12 | Property changeType As %String;
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/discarded.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/discarded.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Build.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.Build
2 | {
3 |
4 | ClassMethod BuildUIForDevMode(devMode As %Boolean, rootDirectory As %String)
5 | {
6 | if 'devMode {
7 | return
8 | }
9 | write !, "In developer mode, building web UI:"
10 | set webUIDirectory = ##class(%File).SubDirectoryName(rootDirectory, "git-webui")
11 | write !, "npm ci"
12 | write !, $zf(-100, "/SHELL", "npm", "ci", "--prefix", webUIDirectory)
13 | write !, "npm run build"
14 | write !, $zf(-100, "/SHELL", "npm", "run", "build", "--prefix", webUIDirectory)
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/context.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/context.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PullEventHandler/PackageManager.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.PullEventHandler.PackageManager Extends SourceControl.Git.PullEventHandler
2 | {
3 |
4 | Parameter NAME = "Package Manager";
5 |
6 | Parameter DESCRIPTION = "Does zpm ""load """;
7 |
8 | /// Subclasses may override to customize behavior on pull.
9 | Method OnPull() As %Status
10 | {
11 | set command = "load "_..LocalRoot
12 | quit $select(
13 | $$$comClassDefined("%IPM.Main"): ##class(%IPM.Main).Shell(command),
14 | 1: ##class(%ZPM.PackageManager).Shell(command)
15 | )
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/js/polyfills.js:
--------------------------------------------------------------------------------
1 | /**
2 | * String.prototype.replaceAll() polyfill
3 | * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
4 | * @author Chris Ferdinandi
5 | * @license MIT
6 | */
7 | if (!String.prototype.replaceAll) {
8 | String.prototype.replaceAll = function(str, newStr){
9 |
10 | // If a regex pattern
11 | if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
12 | return this.replace(str, newStr);
13 | }
14 |
15 | // If a string
16 | return this.replace(new RegExp(str, 'g'), newStr);
17 |
18 | };
19 | }
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/js/polyfills.js:
--------------------------------------------------------------------------------
1 | /**
2 | * String.prototype.replaceAll() polyfill
3 | * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/
4 | * @author Chris Ferdinandi
5 | * @license MIT
6 | */
7 | if (!String.prototype.replaceAll) {
8 | String.prototype.replaceAll = function(str, newStr){
9 |
10 | // If a regex pattern
11 | if (Object.prototype.toString.call(str).toLowerCase() === '[object regexp]') {
12 | return this.replace(str, newStr);
13 | }
14 |
15 | // If a string
16 | return this.replace(new RegExp(str, 'g'), newStr);
17 |
18 | };
19 | }
--------------------------------------------------------------------------------
/cls/_zpkg/isc/sc/git/SystemMode.cls:
--------------------------------------------------------------------------------
1 | Class %zpkg.isc.sc.git.SystemMode
2 | {
3 | ClassMethod SetEnvironment(environment As %String) As %Status [ NotInheritable, Private ]
4 | {
5 | $$$AddAllRoleTemporary
6 | do $SYSTEM.Version.SystemMode(environment)
7 | new $namespace
8 | set $namespace = "%SYS"
9 | set obj = ##class(Config.Startup).Open()
10 | set obj.SystemMode = environment
11 | return obj.%Save()
12 | }
13 |
14 | ClassMethod SetSystemMode(environment As %String) As %Status [ NotInheritable ]
15 | {
16 | try {
17 | do ..SetEnvironment(environment)
18 | } catch e {
19 | return e.AsStatus()
20 | }
21 | return $$$OK
22 | }
23 | }
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/inboxes.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/inboxes.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/_resources/cls/UnitTest/SampleProduction.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SampleProduction Extends Ens.Production
2 | {
3 |
4 | XData ProductionDefinition
5 | {
6 |
7 |
8 | 60
9 |
10 |
11 | 61
12 |
13 |
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PullEventHandler/Default.cls:
--------------------------------------------------------------------------------
1 | Include SourceControl.Git
2 |
3 | Class SourceControl.Git.PullEventHandler.Default Extends (SourceControl.Git.PullEventHandler.IncrementalLoad, SourceControl.Git.PullEventHandler.PackageManager)
4 | {
5 |
6 | Parameter NAME = "Default";
7 |
8 | Parameter DESCRIPTION = "Does a zpm ""load "" for PackageManager-enabled repos and an incremental load otherwise.";
9 |
10 | Method OnPull() As %Status
11 | {
12 | if ##class(%Library.File).Exists(##class(%Library.File).NormalizeFilename("module.xml",..LocalRoot)) {
13 | quit ##class(SourceControl.Git.PullEventHandler.PackageManager)$this.OnPull()
14 | }
15 | quit ##class(SourceControl.Git.PullEventHandler.IncrementalLoad)$this.OnPull()
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/gear-fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/gear-fill.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/git-webui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-webui",
3 | "version": "1.3.0",
4 | "description": "A web user interface for git",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "grunt release"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git@github.com:alberthier/git-webui.git"
13 | },
14 | "keywords": [
15 | "git",
16 | "gui"
17 | ],
18 | "author": "Éric ALBER ",
19 | "license": "Apache-2.0",
20 | "devDependencies": {
21 | "grunt": "^1.4.1",
22 | "grunt-contrib-clean": "^2.0.0",
23 | "grunt-contrib-copy": "^1.0.0",
24 | "grunt-contrib-less": "^3.0.0",
25 | "grunt-contrib-watch": "^1.1.0"
26 | },
27 | "dependencies": {
28 | "jquery": "^3.5.1",
29 | "popper.js": "^1.16.1-lts",
30 | "bootstrap": "^4.6.0"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/StreamServer.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.StreamServer Extends %CSP.StreamServer
2 | {
3 |
4 | /// The OnPage() is called by the CSP dispatcher to generate the
5 | /// page content. For %CSP.StreamServer, since the content type is actually a stream, not HTML
6 | /// we simply write out the stream data.
7 | ClassMethod OnPage() As %Status
8 | {
9 | if (%stream '= $$$NULLOREF) && $data(%base)#2 {
10 | set sourceControlInclude = ##class(SourceControl.Git.Utils).GetSourceControlInclude()
11 | set bodyAttrs = ##class(SourceControl.Git.Utils).ProductionConfigBodyAttributes()
12 | set configScript = ##class(SourceControl.Git.Utils).ProductionConfigScript()
13 | while '%stream.AtEnd {
14 | set text = %stream.Read(1000000)
15 | set text = $replace(text,"{{baseHref}}",..EscapeHTML(%base))
16 | set text = $replace(text,"{{bodyAttrs}}",bodyAttrs)
17 | write $replace(text,"{{sourceControlInclude}}",sourceControlInclude_$$$NL_configScript)
18 | }
19 | quit $$$OK
20 | }
21 | quit ##super()
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 InterSystems Corporation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/DeploymentLog.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.DeploymentLog Extends %Persistent [ Owner = {%Developer} ]
2 | {
3 |
4 | Property Token As %String [ InitialExpression = {$System.Util.CreateGUID()} ];
5 |
6 | Property StartTimestamp As %TimeStamp;
7 |
8 | Property EndTimestamp As %TimeStamp;
9 |
10 | Property HeadRevision As %String;
11 |
12 | Property Status As %Status;
13 |
14 | Storage Default
15 | {
16 |
17 |
18 | %%CLASSNAME
19 |
20 |
21 | Token
22 |
23 |
24 | StartTimestamp
25 |
26 |
27 | EndTimestamp
28 |
29 |
30 | HeadRevision
31 |
32 |
33 | Status
34 |
35 |
36 | ^SourceContro22B9.DeploymentLogD
37 | DeploymentLogDefaultData
38 | ^SourceContro22B9.DeploymentLogD
39 | ^SourceContro22B9.DeploymentLogI
40 | ^SourceContro22B9.DeploymentLogS
41 | %Storage.Persistent
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PullEventHandler/PackageManagerReload.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.PullEventHandler.PackageManagerReload Extends SourceControl.Git.PullEventHandler
2 | {
3 |
4 | Parameter NAME = "Package Manager Reload";
5 |
6 | Parameter DESCRIPTION = "Does zpm ""uninstall"", then zpm ""load """;
7 |
8 | /// Subclasses may override to customize behavior on pull.
9 | Method OnPull() As %Status
10 | {
11 | set moduleFilePath = ##class(%File).NormalizeFilename("module.xml",..LocalRoot)
12 | set sc = $System.OBJ.Load(moduleFilePath,"-d",,.internalName,1) // list-only load to get module name
13 | $$$QuitOnError(sc)
14 | set context = ##class(SourceControl.Git.PackageManagerContext).ForInternalName(internalName)
15 | if $isobject(context.Package) {
16 | set command = "uninstall "_context.Package.Name
17 | set sc = $select(
18 | $$$comClassDefined("%IPM.Main"): ##class(%IPM.Main).Shell(command),
19 | 1: ##class(%ZPM.PackageManager).Shell(command)
20 | )
21 | $$$QuitOnError(sc)
22 | }
23 | set command = "load "_..LocalRoot
24 | quit $select(
25 | $$$comClassDefined("%IPM.Main"): ##class(%IPM.Main).Shell(command),
26 | 1: ##class(%ZPM.PackageManager).Shell(command)
27 | )
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/test/_resources/ptd/UnitTest_SampleProduction/Stgs-c949C.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | UnitTest.SampleProduction
15 | 1841-01-01 00:00:00.000
16 |
17 |
18 |
19 |
20 | Settings-c
21 | Settings:c.PTD
22 |
23 |
24 |
25 |
26 | ]]>
27 |
28 |
29 |
30 |
31 | ]]>
32 |
33 |
--------------------------------------------------------------------------------
/test/_resources/ptd/UnitTest_SampleProduction/Stgs-b949C.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | UnitTest.SampleProduction
15 | 1841-01-01 00:00:00.000
16 |
17 |
18 |
19 |
20 | Settings-b
21 | Settings:b.PTD
22 |
23 |
24 |
25 |
26 | ]]>
27 |
28 |
29 |
30 | 71
31 | ]]>
32 |
33 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/Extension.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.Extension Extends UnitTest.SourceControl.Git.AbstractTest
2 | {
3 |
4 | Method TestGeneratedFilesReadOnlyOption()
5 | {
6 | set sc = $$$OK
7 | set classCreated = 0
8 | try {
9 | do $System.OBJ.Delete("UnitTest.SourceControl.Git.GeneratedReadOnly")
10 | set classDef = ##class(%Dictionary.ClassDefinition).%New()
11 | set classDef.Name = "UnitTest.SourceControl.Git.GeneratedReadOnly"
12 | set classDef.GeneratedBy = "UnitTest.SourceControl.Git.GeneratedBy"
13 | $$$ThrowOnError(classDef.%Save())
14 | $$$ThrowOnError($System.OBJ.Compile("UnitTest.SourceControl.Git.GeneratedReadOnly", "ck"))
15 | set classCreated = 1
16 |
17 | set settings = ##class(SourceControl.Git.Settings).%New()
18 | set settings.generatedFilesReadOnly = 0
19 | $$$ThrowOnError(settings.%Save())
20 | set extension = ##class(SourceControl.Git.Extension).%New("")
21 | do $$$AssertNotTrue(extension.IsReadOnly("UnitTest.SourceControl.Git.GeneratedReadOnly.cls"))
22 |
23 | set settings.generatedFilesReadOnly = 1
24 | $$$ThrowOnError(settings.%Save())
25 | do $$$AssertTrue(extension.IsReadOnly("UnitTest.SourceControl.Git.GeneratedReadOnly.cls"))
26 | } catch err {
27 | set sc = err.AsStatus()
28 | }
29 | if classCreated {
30 | do $System.OBJ.Delete("UnitTest.SourceControl.Git.GeneratedReadOnly")
31 | }
32 | $$$ThrowOnError(sc)
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/AbstractTest.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.AbstractTest Extends %UnitTest.TestCase
2 | {
3 |
4 | Property InitialExtension As %String [ InitialExpression = {##class(%Studio.SourceControl.Interface).SourceControlClassGet()} ];
5 |
6 | Property SourceControlGlobal [ MultiDimensional ];
7 |
8 | Method %OnNew(initvalue) As %Status
9 | {
10 | Merge ..SourceControlGlobal = ^SYS("SourceControl")
11 | Kill ^SYS("SourceControl")
12 | Set settings = ##class(SourceControl.Git.Settings).%New()
13 | Set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"dir"
14 | Set settings.Mappings("CLS","*")="cls/"
15 | Do settings.%Save()
16 | Do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension")
17 | Quit ##super(initvalue)
18 | }
19 |
20 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
21 | {
22 | Do ##class(%Studio.SourceControl.Interface).SourceControlClassSet(..InitialExtension)
23 | Kill ^SYS("SourceControl")
24 | Merge ^SYS("SourceControl") = ..SourceControlGlobal
25 | Quit $$$OK
26 | }
27 |
28 | ClassMethod WriteFile(filePath, contents)
29 | {
30 | set dirPath = ##class(%File).GetDirectory(filePath)
31 | if '##class(%File).CreateDirectoryChain(dirPath,.ret) {
32 | $$$ThrowStatus($$$ERROR($$$GeneralError,"failed to create directory: "_ret))
33 | }
34 | set fileStream = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc)
35 | $$$ThrowOnError(sc)
36 | do fileStream.Write(contents)
37 | $$$ThrowOnError(fileStream.%Save())
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/cls/_zpkg/isc/sc/git/Defaults.cls:
--------------------------------------------------------------------------------
1 | Class %zpkg.isc.sc.git.Defaults
2 | {
3 |
4 | ClassMethod GetDefaults() As %Library.DynamicObject [ NotInheritable, Private ]
5 | {
6 | set defaults = {}
7 | set storage = "^%SYS(""SourceControl"",""Git"",""defaults"")"
8 | $$$AddAllRoleTemporary
9 |
10 | set key = $order(@storage@(""))
11 | while key '= "" {
12 | do defaults.%Set(key, $get(@storage@(key)))
13 | set key = $order(@storage@(key))
14 | }
15 | return defaults
16 | }
17 |
18 | ClassMethod GetDefaultSettings(ByRef defaults As %Library.DynamicObject) As %Status
19 | {
20 | try {
21 | set defaults = ..GetDefaults()
22 | } catch e {
23 | return e.AsStatus()
24 | }
25 | return $$$OK
26 | }
27 |
28 | ClassMethod SetDefaults(defaults As %Library.DynamicObject) As %Status [ NotInheritable, Private ]
29 | {
30 |
31 | $$$AddAllRoleTemporary
32 | set storage = "^%SYS(""SourceControl"",""Git"",""defaults"")"
33 | k @storage
34 | set iterator = defaults.%GetIterator()
35 |
36 | while iterator.%GetNext(.key, .value) {
37 | set @storage@(key) = value
38 | }
39 |
40 | return $$$OK
41 | }
42 |
43 | ClassMethod SetDefaultSettings(defaults As %Library.DynamicObject) As %Status [ NotInheritable ]
44 | {
45 |
46 | set newDefaults = {}
47 |
48 | set iterator = defaults.%GetIterator()
49 |
50 | while iterator.%GetNext(.key, .value) {
51 | do newDefaults.%Set(key, value)
52 | }
53 |
54 | try {
55 | do ..SetDefaults(newDefaults)
56 | } catch e {
57 | return e.AsStatus()
58 | }
59 | return $$$OK
60 | }
61 |
62 | }
--------------------------------------------------------------------------------
/test/_resources/ptd/UnitTest_SampleProduction/ProdStgs-UnitTest_SampleProduction.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | UnitTest.SampleProduction
15 | 1841-01-01 00:00:00.000
16 |
17 |
18 |
19 |
20 | ProductionSettings-UnitTest_SampleProduction
21 | ProductionSettings:UnitTest.SampleProduction.PTD
22 |
23 |
24 |
25 |
26 | ]]>
27 |
28 |
29 |
30 |
31 | ]]>
32 |
33 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute
2 |
3 | Thank you for your interest in contributing! While this project originated at InterSystems, it is our hope that the community will continue to extend and enhance it.
4 |
5 | ## Submitting changes
6 |
7 | If you have made a change that you would like to contribute back to the community, please send a [GitHub Pull Request](/pull/new/main) explaining it. If your change fixes an issue that you or another user reported, please mention it in the pull request. You can find out more about pull requests [here](http://help.github.com/pull-requests/).
8 |
9 | Every pull request should include at least one entry in CHANGELOG.md - see [keepachangelog.com](https://keepachangelog.com/) for guidelines.
10 |
11 | We encourage use of [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/).
12 |
13 | ## Coding conventions
14 |
15 | Generally speaking, just try to match the conventions you see in the code you are reading. For this project, these include:
16 |
17 | * Do not use shortened command and function names. For example, use `set` instead of `s` and `$piece` instead of `$p`
18 | * One command per line
19 | * Do not use dot syntax
20 | * Indentation with tabs
21 | * [Pascal case](https://en.wikipedia.org/wiki/Camel_case) class and method names
22 | * Avoid using postconditionals
23 | * Local variables start with `t`; formal parameter names start with `p`
24 | * Always check %Status return values
25 |
26 | When making changes that involve JavaScript, ensure that your changes still work from Studio (which uses an old version of IE under the hood and therefore doesn't support various things you might take for granted).
27 |
28 | Thank you!
--------------------------------------------------------------------------------
/docs/baselining.md:
--------------------------------------------------------------------------------
1 | # Baselining Source Code
2 | Baselining source code is the first step to enabling source control on an existing system. Baselining synchronizes the Git repository with a source of truth, usually the current state of the production environment. This establishes a clean starting point so that any future changes can be tracked.
3 |
4 | Git-source-control includes an API method `BaselineExport` that may be run in an IRIS terminal to export items in a namespace to the configured Git repository.
5 |
6 | A baselining workflow will commonly include these steps:
7 | - Create a new remote repository on your Git platform of choice.
8 | - Use `do ##class(SourceControl.Git.API).Configure()` to configure Git on the production environment (or a copy of the production environment). Clone the new remote repository.
9 | - Use the Settings page in the Source Control menu to customize the mapping configuration.
10 | - Use the Source Control menu to check out a new branch for the baseline export.
11 | - Use the `BaselineExport` method to export all items to the Git repository, commit, and push to the remote: `do ##class(SourceControl.Git.API).BaselineExport("initial baseline commit","origin")`
12 | - Create a merge or pull request on your remote Git platform from the baseline branch to the main branch. Review the code to ensure it includes all required items. If needed, modify the mapping configuration and redo the baseline export. When the baseline is satisfactory, merge it into the main branch.
13 | - Use the Source Control menu to switch back to the main branch on the production environment.
14 | - For each other environment, configure Git to clone that same remote repository. From the Source Control menu, run Import All (Force) to load all items from the baseline.
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/Initialization.cls:
--------------------------------------------------------------------------------
1 | Include %sySecurity
2 |
3 | Class UnitTest.SourceControl.Git.Initialization Extends %UnitTest.TestCase
4 | {
5 |
6 | Method TestSetupFavorites()
7 | {
8 | Set page = "Git: "_$Namespace
9 | Set username = $Username
10 | &sql(delete from %SYS_Portal.Users where Page = :page and Username = :username)
11 | // Intentionally called twice!
12 | Do ##class(SourceControl.Git.Utils).ConfigureWeb()
13 | Do ##class(SourceControl.Git.Utils).ConfigureWeb()
14 | &sql(select count(*) into :favoriteCount from %SYS_Portal.Users where Page = :page and Username = :username)
15 | Do $$$AssertEquals(favoriteCount,1)
16 | Do $$$AssertTrue($$$SecurityApplicationsExists("/isc/studio/usertemplates",record))
17 | Do $$$AssertEquals($$$GetSecurityApplicationsGroupById(record),"%ISCMgtPortal")
18 | }
19 |
20 | Method TestRunGitInGarbageContext()
21 | {
22 | Set settings = ##class(SourceControl.Git.Settings).%New()
23 | Set oldTemp = settings.namespaceTemp
24 | Set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"nonexistentdir"
25 | set settings.environmentName = ""
26 | Do $$$AssertStatusOK(settings.%Save())
27 | Try {
28 | Do ##class(%Library.File).RemoveDirectory(settings.namespaceTemp)
29 | // This is a prerequisite in any testing environment.
30 | Write ##class(SourceControl.Git.Utils).TempFolder()
31 | Do $$$AssertTrue(##class(SourceControl.Git.Utils).GitBinExists())
32 | } Catch e {
33 | Do $$$AssertFailure("Error occurred: "_$System.Status.GetErrorText(e.AsStatus()))
34 | }
35 | // OK for unit test to leak this if it was empty to start
36 | If (oldTemp '= "") {
37 | Set settings.namespaceTemp = oldTemp
38 | Do $$$AssertStatusOK(settings.%Save())
39 | }
40 | }
41 |
42 | }
43 |
44 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Installer.cls:
--------------------------------------------------------------------------------
1 | /// Mostly used from SourceControl.Git.API:MapEverywhere
2 | Class SourceControl.Git.Installer
3 | {
4 |
5 | ClassMethod MapEverywhere() As %Status
6 | {
7 | set sc = $$$OK
8 | try {
9 | set ns = $namespace
10 | set locDBDir = ##class(%SYS.Namespace).GetGlobalDest(ns,$Name(^IRIS.Msg("Studio")))
11 | set vars("LocalizationDB") = ..DatabaseDirToName(locDBDir)
12 | set codeDBDir = ##class(%SYS.Namespace).GetPackageDest(ns,"SourceControl.Git")
13 | set vars("RoutineDB") = ..DatabaseDirToName(codeDBDir)
14 | $$$ThrowOnError(..RunMapEverywhere(.vars))
15 | } catch e {
16 | set sc = e.AsStatus()
17 | if '$quit {
18 | write !,$System.Status.GetErrorText(sc)
19 | }
20 | }
21 | quit sc
22 | }
23 |
24 | ClassMethod DatabaseDirToName(dbDir As %String) As %String [ Private ]
25 | {
26 | New $Namespace
27 | Set $Namespace = "%SYS"
28 | Set tSC = ##class(Config.Databases).DatabasesByDirectory($Piece(dbDir,"^"),$Piece(dbDir,"^",2),.tDBList)
29 | $$$ThrowOnError(tSC)
30 | If ($ListLength(tDBList) '= 1) {
31 | // This is highly unexpected, but worth checking for anyway.
32 | $$$ThrowStatus($$$ERROR($$$GeneralError,$$$FormatText("Could not find database name for '%1'",tDBDir)))
33 | }
34 | $$$ThrowOnError(tSC)
35 | Quit $ListGet(tDBList)
36 | }
37 |
38 | /// This is a method generator whose code is generated by XGL.
39 | ClassMethod RunMapEverywhere(ByRef pVars, pLogLevel As %Integer = 3, pInstaller As %Installer.Installer, pLogger As %Installer.AbstractLogger) As %Status [ CodeMode = objectgenerator, Internal, Private ]
40 | {
41 | quit ##class(%Installer.Manifest).%Generate(%compiledclass, %code, "MapEverywhere")
42 | }
43 |
44 | XData MapEverywhere [ XMLNamespace = INSTALLER ]
45 | {
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Util/XMLConflictResolver.cls:
--------------------------------------------------------------------------------
1 | Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
2 |
3 | Class SourceControl.Git.Util.XMLConflictResolver Extends %RegisteredObject
4 | {
5 |
6 | Parameter ExpectedConflictTag;
7 |
8 | Parameter OutputIndent;
9 |
10 | Method ResolveStream(stream As %Stream.Object)
11 | {
12 | // File may have:
13 | /*
14 | <<<<<<< HEAD
15 |
16 | =======
17 |
18 | >>>>>>> 607d1f6 (modified src/HCC/Connect/Production.cls add Demo5)
19 |
20 | */
21 |
22 | // If:
23 | // * We have one such marker (<<<<<<< / ======= / >>>>>>>)
24 | // * The line after >>>>>> is ""
25 | // Then:
26 | // * We can replace ======= with ""
27 |
28 | Set copy = ##class(%Stream.TmpCharacter).%New()
29 | Set markerCount = 0
30 | Set postCloseMarker = 0
31 | While 'stream.AtEnd {
32 | Set line = stream.ReadLine()
33 | Set start = $Extract(line,1,7)
34 | If start = "<<<<<<<" {
35 | Set markerCount = markerCount + 1
36 | Continue
37 | } ElseIf (start = ">>>>>>>") {
38 | Set postCloseMarker = 1
39 | Continue
40 | } ElseIf (start = "=======") {
41 | Do copy.WriteLine(..#OutputIndent_..#ExpectedConflictTag)
42 | Continue
43 | } ElseIf postCloseMarker {
44 | If $ZStrip(line,"<>W") '= ..#ExpectedConflictTag {
45 | $$$ThrowStatus($$$ERROR($$$GeneralError,"The type of conflict encountered is not handled; user must resolve manually."))
46 | }
47 | Set postCloseMarker = 0
48 | }
49 | Do copy.WriteLine(line)
50 | }
51 |
52 | If markerCount > 1 {
53 | $$$ThrowStatus($$$ERROR($$$GeneralError,"Multiple conflicts found, cannot resolve automatically."))
54 | } ElseIf markerCount = 0 {
55 | $$$ThrowStatus($$$ERROR($$$GeneralError,"No conflict markers found in file"))
56 | }
57 |
58 | $$$ThrowOnError(stream.CopyFromAndSave(copy))
59 |
60 | Quit 1
61 | }
62 |
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/Util/Production.cls:
--------------------------------------------------------------------------------
1 | Include SourceControl.Git
2 |
3 | Class UnitTest.SourceControl.Git.Util.Production Extends %UnitTest.TestCase
4 | {
5 |
6 | Property Mappings [ MultiDimensional ];
7 |
8 | Method TestItemIsPTD()
9 | {
10 | do $$$AssertNotTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("cls/test.xml"))
11 | do $$$AssertNotTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd/test.md"))
12 | do $$$AssertNotTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD(""))
13 | do $$$AssertTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd/test.xml"))
14 | do $$$AssertTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd2/test.xml"))
15 | do $$$AssertTrue(##class(SourceControl.Git.Util.Production).ItemIsPTD("ptd2\test.xml"))
16 | }
17 |
18 | Method TestLoadProductionsFromDirectory()
19 | {
20 | // load a production from a class file under resources
21 | set packageRoot = ##class(SourceControl.Git.PackageManagerContext).ForInternalName("git-source-control.zpm").Package.Root
22 | $$$ThrowOnError($System.OBJ.Load(packageRoot_"test/_resources/cls/UnitTest/SampleProduction.cls","ck"))
23 | // call LoadProductionsFromDirectory on a directory under resources/ptd
24 | do $$$AssertStatusOK(##class(SourceControl.Git.Util.Production).LoadProductionsFromDirectory(packageRoot_"test/_resources/ptd"))
25 | // confirm items were deleted and added
26 | set itemA = ##class(Ens.Config.Production).OpenItemByConfigName("UnitTest.SampleProduction||a")
27 | do $$$AssertNotTrue($isobject(itemA),"item a was deleted")
28 | set itemB = ##class(Ens.Config.Production).OpenItemByConfigName("UnitTest.SampleProduction||b")
29 | do $$$AssertEquals(itemB.Settings.GetAt(1).Value,71)
30 | set itemB = ##class(Ens.Config.Production).OpenItemByConfigName("UnitTest.SampleProduction||c")
31 | do $$$AssertTrue($isobject(itemB),"item a was created")
32 | }
33 |
34 | Method OnBeforeAllTests() As %Status
35 | {
36 | merge ..Mappings = @##class(SourceControl.Git.Utils).MappingsNode()
37 | kill @##class(SourceControl.Git.Utils).MappingsNode()
38 | set $$$SourceMapping("PTD", "*") = "ptd/"
39 | set $$$SourceMapping("PTD", "Some.Production") = "ptd2/"
40 | quit $$$OK
41 | }
42 |
43 | Method %OnClose() As %Status
44 | {
45 | kill @##class(SourceControl.Git.Utils).MappingsNode()
46 | merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings
47 | quit $$$OK
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/docs/scintro.md:
--------------------------------------------------------------------------------
1 | # A Brief Introduction to Source Control
2 |
3 | Source control is a system that helps teams manage and track changes to their work, with particulaar relevancy to software development. It helps produce a definitive record of the hostory of the product, as well as allow for revisiting earlier versions and previous changes. This means that multiple people can work on the same thing together without getting in each other's way. You can see who made changes, when they were made, and what these changes were addressing. If any change leads to issues, this allows teams to quickly roll back to a previous version that is working, as well as identify which specific change may have caused the issue.
4 |
5 | Within InterSystems, we use the term "Change Control" referring to the ITIL concept of "Change Management." The goal of "Change Control" is to "establish standard procedures for managing change requests in an agile and efficient manner in an effort to drastically minimize the risk and impact a change can have on business operations." Source control is an important part of achieving Change Control, because it provides granular tracking of individual changes.
6 |
7 | ## Key Concepts
8 |
9 | ### Versioning
10 |
11 | Each change or batch of changes is saved as a new version. This allows users and developers to reference or revert to previous versions when necessary. git-source-control is itself versioned.
12 |
13 | ### History Tracking
14 |
15 | Since every change is logged, source control provides users with a complete history of a project, allowing anyone to track its evolution, as well as pinpoint causes and the nature of issues that may arise
16 |
17 | ### Branching
18 |
19 | Branching involves creating distinct copies of files in the same project, so that changes can be made and tested in isolation, without affecting other users and changes. These "branches" can be merged to bring changes back to the main copy of the files.
20 |
21 | ### Example of source control
22 |
23 | A teammate and I are working on an article. I am focusing on the introduction and conclusion, while they focus on the main paragraph. To avoid conflicts, we create seperate copies (branches) of the article to work on. When I am finished with my work, I can merge the changes I have made back into the main article. Others can see the exact changes I have made, and if anyone else is working on the same section we won't run into problems because I have my own separate copy.
24 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/Pull.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.Pull Extends UnitTest.SourceControl.Git.AbstractTest
2 | {
3 |
4 | Method TestPull()
5 | {
6 | // initialize remote repository on filesystem
7 | set remoteDir = ##class(%Library.File).TempFilename()_"d"
8 | if '##class(%File).CreateDirectoryChain(remoteDir_"/cls",.ret) {
9 | $$$ThrowStatus($$$ERROR($$$GeneralError,"failed to create directory: "_ret))
10 | }
11 | do ..WriteFile(remoteDir_"/cls/TestGit/SampleClass1.cls","Class TestGit.SampleClass1 {}")
12 | do ..WriteFile(remoteDir_"/cls/TestGit/SampleClass2.cls","Class TestGit.SampleClass2 {}")
13 | do $zf(-100,"/SHELL","git","init",remoteDir)
14 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "config", "user.email", "unittest@example.com")
15 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "config", "user.name", "Unit Test")
16 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "add", ".")
17 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "commit", "-m", "initial commit in remote for unit test")
18 | // initialize local repo, cloning remote.
19 | $$$ThrowOnError(##class(SourceControl.Git.Utils).Clone(remoteDir_"/.git"))
20 | // import all and confirm classes exist
21 | do $System.OBJ.Delete("TestGit.SampleClass1,TestGit.SampleClass2")
22 | $$$ThrowOnError(##class(SourceControl.Git.Utils).ImportAll(1))
23 | do $$$AssertTrue($$$comClassDefined("TestGit.SampleClass1"))
24 | do $$$AssertTrue($$$comClassDefined("TestGit.SampleClass2"))
25 | // delete, add, and modify classes on remote. add and commit them all on remote.
26 | if '##class(%File).Delete(remoteDir_"/cls/TestGit/SampleClass1.cls",.ret) {
27 | $$$ThrowStatus($$$ERROR($$$GeneralError,"failed to delete class file"))
28 | }
29 | do ..WriteFile(remoteDir_"/cls/TestGit/SampleClass2.cls","Class TestGit.SampleClass2 { Parameter foo = ""bar""; }")
30 | do ..WriteFile(remoteDir_"/cls/TestGit/SampleClass3.cls","Class TestGit.SampleClass3 {}")
31 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "add", ".")
32 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "commit", "-m", "delete, modify, and add classes on remote")
33 | // pull on local and confirm changes were loaded.
34 | $$$ThrowOnError(##class(SourceControl.Git.API).Pull())
35 | do $$$AssertNotTrue($$$comClassDefined("TestGit.SampleClass1"))
36 | do $$$AssertEquals(##class(TestGit.SampleClass2).#foo, "bar")
37 | do $$$AssertTrue($$$comClassDefined("TestGit.SampleClass3"))
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/BaselineExport.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.BaselineExport Extends %UnitTest.TestCase
2 | {
3 |
4 | Method TestBaselineExport()
5 | {
6 | // create a mac routine
7 | if '##class(%Routine).Exists("test.mac") {
8 | set r = ##class(%Routine).%New("test.mac")
9 | do r.WriteLine(" write 22,!")
10 | do r.Save()
11 | do r.Compile()
12 | }
13 | // create an inc routine
14 | if '##class(%Routine).Exists("test.inc") {
15 | set r = ##class(%Routine).%New("test.inc")
16 | do r.WriteLine(" ; test include routine")
17 | do r.Save()
18 | do r.Compile()
19 | }
20 | // create a class
21 | if '##class(%Dictionary.ClassDefinition).%OpenId("TestPkg.Class") {
22 | set class = ##class(%Dictionary.ClassDefinition).%New()
23 | set class.Name = "TestPkg.Class"
24 | $$$ThrowOnError(class.%Save())
25 | do $system.OBJ.Compile("TestPkg.Class")
26 | }
27 | do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl("test.mac"))
28 | do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl("test.inc"))
29 | do $$$AssertNotTrue(##class(SourceControl.Git.Utils).IsInSourceControl("TestPkg.Class.cls"))
30 | do $$$AssertStatusOK(##class(SourceControl.Git.API).BaselineExport())
31 | do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("test.mac"))
32 | do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("test.inc"))
33 | do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("TestPkg.Class.cls"))
34 | }
35 |
36 | Property InitialExtension As %String [ InitialExpression = {##class(%Studio.SourceControl.Interface).SourceControlClassGet()} ];
37 |
38 | Property SourceControlGlobal [ MultiDimensional ];
39 |
40 | Method %OnNew(initvalue) As %Status
41 | {
42 | Merge ..SourceControlGlobal = ^SYS("SourceControl")
43 | Kill ^SYS("SourceControl")
44 | Set settings = ##class(SourceControl.Git.Settings).%New()
45 | Set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"dir"
46 | Set settings.Mappings("MAC","*")="rtn/"
47 | Set settings.Mappings("CLS","*")="cls/"
48 | Do settings.%Save()
49 | Do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension")
50 | Quit ##super(initvalue)
51 | }
52 |
53 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
54 | {
55 | Do ##class(%Studio.SourceControl.Interface).SourceControlClassSet(..InitialExtension)
56 | Kill ^SYS("SourceControl")
57 | Merge ^SYS("SourceControl") = ..SourceControlGlobal
58 | Quit $$$OK
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/test/_resources/dfi/test2.pivot.dfi:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/csp/webuidriver.csp:
--------------------------------------------------------------------------------
1 |
60 | new $NAMESPACE
61 | set $NAMESPACE = %namespace
62 | if $Get(%stream) {
63 | Quit ##class(SourceControl.Git.StreamServer).Page()
64 | } elseif $IsObject($Get(%data)) {
65 | do %data.OutputToDevice()
66 | }
67 | quit 1
68 |
--------------------------------------------------------------------------------
/module.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | git-source-control
6 | 2.15.0
7 | Server-side source control extension for use of Git on InterSystems platforms
8 | git source control studio vscode
9 | module
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | #{..DeveloperMode}
22 | #{..Root}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/tag.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/tag.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PullEventHandler.cls:
--------------------------------------------------------------------------------
1 | /// Base class for all event handlers for git pull commands.
2 | /// Subclasses may override to perform an incremental load/compile, take no action, do a zpm "load", etc.
3 | Class SourceControl.Git.PullEventHandler Extends %RegisteredObject
4 | {
5 |
6 | Parameter NAME [ Abstract ];
7 |
8 | Parameter DESCRIPTION [ Abstract ];
9 |
10 | /// Local git repo root directory
11 | Property LocalRoot As %String(MAXLEN = "");
12 |
13 | /// Modified files (integer-subscripted array storing objects of class SourceControl.Git.Modification)
14 | Property ModifiedFiles [ MultiDimensional ];
15 |
16 | /// The branch that is checked out before OnPull() is called
17 | Property Branch [ InitialExpression = {##class(SourceControl.Git.Utils).GetCurrentBranch()} ];
18 |
19 | Method OnPull() As %Status [ Abstract ]
20 | {
21 | }
22 |
23 | /// files is an integer-subscripted array of SourceControl.Git.Modification objects.
24 | /// pullEventClass: if defined, override the configured pull event class
25 | ClassMethod ForModifications(ByRef files, pullEventClass As %String) As %Status
26 | {
27 | set st = $$$OK
28 | try {
29 | set log = ##class(SourceControl.Git.DeploymentLog).%New()
30 | set log.HeadRevision = ##class(SourceControl.Git.Utils).GetCurrentRevision()
31 | set log.StartTimestamp = $zdatetime($ztimestamp,3)
32 | set st = log.%Save()
33 | quit:$$$ISERR(st)
34 | set event = $classmethod(
35 | $select(
36 | $data(pullEventClass)#2: pullEventClass,
37 | 1: ##class(SourceControl.Git.Utils).PullEventClass())
38 | ,"%New")
39 | set event.LocalRoot = ##class(SourceControl.Git.Utils).TempFolder()
40 | merge event.ModifiedFiles = files
41 | set st = event.OnPull()
42 | set log.EndTimestamp = $zdatetime($ztimestamp,3)
43 | set log.Status = st
44 | set st = log.%Save()
45 | quit:$$$ISERR(st)
46 | } catch err {
47 | set st = err.AsStatus()
48 | }
49 | quit st
50 | }
51 |
52 | /// InternalName may be a comma-delimited string or $ListBuild list
53 | ClassMethod ForInternalNames(InternalName As %String) As %Status
54 | {
55 | set list = $select($listvalid(InternalName):InternalName,1:$ListFromString(InternalName))
56 | set pointer = 0
57 | while $listnext(list,pointer,InternalName) {
58 | set mod = ##class(SourceControl.Git.Modification).%New()
59 | set mod.internalName = InternalName
60 | set mod.externalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
61 | set mod.changeType = "M"
62 | set files($i(files)) = mod
63 | }
64 | quit ..ForModifications(.files)
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/git-webui/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | grunt.initConfig({
3 | pkg: grunt.file.readJSON('package.json'),
4 |
5 | copy: {
6 | jquery: {
7 | expand: true,
8 | flatten: true,
9 | src: 'node_modules/jquery/dist/jquery.min.js',
10 | dest: 'dist/share/git-webui/webui/js/',
11 | },
12 | bootstrapjs: {
13 | expand: true,
14 | flatten: true,
15 | src: 'node_modules/bootstrap/dist/js/bootstrap.min.js',
16 | dest: 'dist/share/git-webui/webui/js/',
17 | },
18 | bootstrapcss: {
19 | expand: true,
20 | flatten: true,
21 | src: 'node_modules/bootstrap/dist/css/bootstrap.min.css',
22 | dest: 'dist/share/git-webui/webui/css/',
23 | },
24 | popperjs: {
25 | expand: true,
26 | flatten: true,
27 | src: 'node_modules/popper.js/dist/umd/popper.min.js',
28 | dest: 'dist/share/git-webui/webui/js/',
29 | },
30 | git_webui: {
31 | options: {
32 | mode: true,
33 | },
34 | expand: true,
35 | cwd: 'src',
36 | src: ['share/**', '!**/less', '!**/*.less'],
37 | dest: 'dist',
38 | },
39 | release: {
40 | options: {
41 | mode: true,
42 | },
43 | expand: true,
44 | cwd: 'dist',
45 | src: '**',
46 | dest: 'release',
47 | },
48 | },
49 |
50 | less: {
51 | files: {
52 | expand: true,
53 | cwd: 'src',
54 | src: 'share/git-webui/webui/css/*.less',
55 | dest: 'dist',
56 | ext: '.css',
57 | },
58 | },
59 |
60 | watch: {
61 | scripts: {
62 | files: ['src/share/**/*.js', 'src/share/**/*.html'],
63 | tasks: 'copy:git_webui'
64 | },
65 | css: {
66 | files: 'src/**/*.less',
67 | tasks: 'less',
68 | },
69 | },
70 |
71 | clean: ['dist'],
72 | });
73 |
74 | grunt.loadNpmTasks('grunt-contrib-copy');
75 | grunt.loadNpmTasks('grunt-contrib-less');
76 | grunt.loadNpmTasks('grunt-contrib-clean');
77 | grunt.loadNpmTasks('grunt-contrib-watch');
78 |
79 | grunt.registerTask('copytodist', ['copy:jquery', 'copy:bootstrapjs', 'copy:bootstrapcss', 'copy:popperjs', 'copy:git_webui']);
80 | grunt.registerTask('default', ['copytodist', 'less']);
81 | grunt.registerTask('release', ['default', 'copy:release']);
82 | };
83 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PackageManagerContext.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.PackageManagerContext Extends SourceControl.Git.Util.Singleton
2 | {
3 |
4 | Property InternalName As %String;
5 |
6 | Property IsInDefaultPackage As %Boolean [ InitialExpression = 0 ];
7 |
8 | Property IsInGitEnabledPackage As %Boolean [ InitialExpression = 0 ];
9 |
10 | /// Really is a %ZPM.PackageManager.Developer.Module / %IPM.Storage.Module
11 | Property Package As %RegisteredObject [ InitialExpression = {$$$NULLOREF} ];
12 |
13 | /// Really is a %ZPM.PackageManager.Developer.ResourceReference / %IPM.Storage.ResourceReference
14 | Property ResourceReference As %RegisteredObject [ InitialExpression = {$$$NULLOREF} ];
15 |
16 | Method InternalNameSet(InternalName As %String = "") As %Status
17 | {
18 | set InternalName = ##class(SourceControl.Git.Utils).NormalizeInternalName(InternalName)
19 | if (InternalName '= i%InternalName) {
20 | set i%InternalName = InternalName
21 | set resourceReference = $$$NULLOREF
22 | if (InternalName = ##class(SourceControl.Git.Settings.Document).#INTERNALNAME) {
23 | // Embedded Git settings document is never in an IPM context
24 | set ..Package = $$$NULLOREF
25 | } elseif $$$comClassDefined("%IPM.ExtensionBase.Utils") {
26 | set ..Package = ##class(%IPM.ExtensionBase.Utils).FindHomeModule(InternalName,,.resourceReference)
27 | } elseif $$$comClassDefined("%ZPM.PackageManager.Developer.Extension.Utils") {
28 | set ..Package = ##class(%ZPM.PackageManager.Developer.Extension.Utils).FindHomeModule(InternalName,,.resourceReference)
29 | } else {
30 | set ..Package = $$$NULLOREF
31 | }
32 | set ..ResourceReference = resourceReference
33 | set ..IsInGitEnabledPackage = $isobject(..Package) && ##class(%Library.File).Exists(##class(%Library.File).NormalizeFilename(".git",..Package.Root))
34 | set ..IsInDefaultPackage = $isobject(..Package) && (##class(%Library.File).NormalizeDirectory(..Package.Root) = ##class(%Library.File).NormalizeDirectory(##class(SourceControl.Git.Utils).DefaultTempFolder()))
35 | }
36 | quit $$$OK
37 | }
38 |
39 | ClassMethod ForInternalName(InternalName As %String = "") As SourceControl.Git.PackageManagerContext
40 | {
41 | set instance = ..%Get()
42 | set instance.InternalName = InternalName
43 | set InternalName = instance.InternalName
44 | quit instance
45 | }
46 |
47 | Method Dump()
48 | {
49 | write !,"Package manager context: "
50 | write !?4,"InternalName: ",..InternalName
51 | write !?4,"Package: ",$select($isobject(..Package):..Package.Name,1:"")
52 | write !?4,"Resource: ",$select($isobject(..ResourceReference):..ResourceReference.Name,1:"")
53 | write !?4,"Default? ",$select(..IsInDefaultPackage:"Yes",1:"No")
54 | write !?4,"Git-enabled? ",$select(..IsInGitEnabledPackage:"Yes",1:"No"),!
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/csp/pull.csp:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Git Fetch, Git Pull, and Load to IRIS
8 |
9 |
10 |
11 |
12 |
Change Context:
27 |
28 |
29 |
30 |
31 |
32 |
33 |
71 |
72 |
--------------------------------------------------------------------------------
/docs/production-decomposition.md:
--------------------------------------------------------------------------------
1 | # Production Decomposition
2 | Production Decomposition is a feature of Embedded Git 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.
3 |
4 |
5 | 
6 |
7 | ## Enabling production decomposition
8 | 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 `embedded-git-config.json` file at the root of the repository that may be committed and imported into other environments.
9 |
10 | If there are existing productions in the namespace, they should be migrated to the new decomposed format. Do this by opening the production and selecting the "Export Production" option in the source control menu. You may then use the Git Web UI to view, commit, and push the corresponding changes. This step should be done in a single namespace and then deployed to other namespaces through normal Embedded Git deployment mechanisms.
11 |
12 | ## Editing productions in the IDE
13 | There are a couple of limitations related to editing a production class directly in an integrated development environment (Studio or VS Code).
14 | - Any elements of the class definition other than the production definition (for example, methods, parameters, or a custom superclass) are not source controlled if production decomposition is enabled. A recommended workaround is to move these items to a separate utility class.
15 | - The hooks in the IDE are not able to detect which specific production items are being edited. As a result, if any item has an uncommitted change from a different user, you will be blocked from editing the production in the IDE entirely.
16 | As a result of these limitations, editing decomposed productions in the IDE is prohibited by default. To enable it, enable the "Decomposed Productions Allow IDE" setting on the settings page.
17 |
18 | ## Known Limitations
19 | - 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.
20 | - Production Decomposition is not supported for deployment of changes to productions using the InterSystems Package Manager.
21 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/AddRemove.cls:
--------------------------------------------------------------------------------
1 | Import SourceControl.Git
2 |
3 | Class UnitTest.SourceControl.Git.AddRemove Extends %UnitTest.TestCase
4 | {
5 |
6 | Method TestReadonlyDelete()
7 | {
8 | new %SourceControl
9 | do ##class(%Studio.SourceControl.Interface).SourceControlCreate()
10 | do ##class(API).Lock()
11 | try {
12 | do %SourceControl.OnBeforeDelete("")
13 | do $$$AssertFailure("No error thrown when deleting in locked environment")
14 | } catch e {
15 | do $$$AssertEquals(e.Name,"Can't delete in locked environment")
16 | }
17 | do ##class(API).Unlock()
18 | }
19 |
20 | Method TestInit()
21 | {
22 | new %SourceControl
23 | do ##class(%Studio.SourceControl.Interface).SourceControlCreate()
24 | set sc = %SourceControl.UserAction(0,"%SourceMenu,Init","","",.action,.target,.msg,.reload)
25 | do $$$AssertStatusOK(sc)
26 | }
27 |
28 | Method TestMac()
29 | {
30 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.mac"),0)
31 | do $$$AssertStatusOK(##class(Utils).AddToSourceControl("test.mac"))
32 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.mac"),1)
33 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.MAC"),1)
34 | do $$$AssertStatusOK(##class(Utils).RemoveFromSourceControl("test.mac"))
35 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.mac"),0)
36 | }
37 |
38 | Method TestMACInUpperCase()
39 | {
40 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.mac"),0)
41 | do $$$AssertStatusOK(##class(Utils).AddToSourceControl("test.MAC"))
42 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.mac"),1)
43 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.MAC"),1)
44 | do $$$AssertStatusOK(##class(Utils).RemoveFromSourceControl("test.mac"))
45 | do $$$AssertEquals(##class(Utils).IsInSourceControl("test.mac"),0)
46 | }
47 |
48 | Method OnBeforeAllTests() As %Status
49 | {
50 | if '##class(%Routine).Exists("test.mac") {
51 | set r = ##class(%Routine).%New("test.mac")
52 | do r.WriteLine(" write 22,!")
53 | do r.Save()
54 | do r.Compile()
55 | }
56 | quit $$$OK
57 | }
58 |
59 | Property InitialExtension As %String [ InitialExpression = {##class(%Studio.SourceControl.Interface).SourceControlClassGet()} ];
60 |
61 | Property SourceControlGlobal [ MultiDimensional ];
62 |
63 | Method %OnNew(initvalue) As %Status
64 | {
65 | Merge ..SourceControlGlobal = ^SYS("SourceControl")
66 | Kill ^SYS("SourceControl")
67 | Set settings = ##class(SourceControl.Git.Settings).%New()
68 | Set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"dir"
69 | Set settings.Mappings("MAC","*")="rtn/"
70 | Do settings.%Save()
71 | Do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension")
72 | Quit ##super(initvalue)
73 | }
74 |
75 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
76 | {
77 | Do ##class(%Studio.SourceControl.Interface).SourceControlClassSet(..InitialExtension)
78 | Kill ^SYS("SourceControl")
79 | Merge ^SYS("SourceControl") = ..SourceControlGlobal
80 | Quit $$$OK
81 | }
82 |
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/docs/expert.md:
--------------------------------------------------------------------------------
1 | ### Feature branches
2 |
3 | The first step in making changes in Health Connect Cloud using git-source-control is making a feature branch. In order for changes to be tracked by source control properly, each change (also called a feature) should be made on it's own branch, so as to not interfere with other changes, and allow for testing of its effects on the production environment. To create a new feature branch, navigate to the Git UI in the namespace you plan on doing development.
4 |
5 | First, make sure that you are in the development branch. It should be bolded and at the top of the Local Branches in the sidebar. If it is not, as pictured below, click on the "development" branch and press checkout branch.
6 |
7 | 
8 |
9 | Once you are in the development branch, press the "+" next the the Local Branches tab, and create a new branch by typing out a name for it. (No spaces or special characters are allowed in branch names).
10 |
11 | 
12 | 
13 |
14 | Finally, you should click on the new branch you just made in the sidebar, and then press "Push".
15 |
16 | 
17 |
18 | This makes sure that your branch will be tracked in Gitlab, so that other users can checkout your branch to test your changes on their own namespaces.
19 |
20 | After this, you can start working on the change.
21 |
22 | ### Merge Requests
23 |
24 | Once you have made all the changes for the specific feature you are working on, and have tested in your namespace, it will be time to merge all of these changes into the development branch. To start, make sure that all your changes have been commited to your feature branch. To do this, navigate to the Git UI, and after making sure you are in the right feature branch, press the "Workspace" tab at the top of the sidebar.
25 |
26 | 
27 |
28 | This will bring you to the workspace view. All the changes you made to files should be in the bottom left of the workspace view. 
29 |
30 | Clicking on one of these files will bring up a line by line view of the changes that have been made. Make sure that these are the changes you want to make, and then select all the files. Next, enter a commit message that should describe that changes that have been made.
31 |
32 | 
33 |
34 | You can enter more details below, and then press the commit button underneath. This will commit the changes you have made to your feature branch. Once all of your changes have been committed, you should be ready to merge into the development branch.
35 |
36 | In the Git UI, first checkout the development branch, by selecting it from the Local Branches and pressing "Checkout branch". Next, find the feature branch that you have been working on, select it from the local branches, and press "Merge Branch".
37 |
38 | 
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/ImportAll.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.ImportAll Extends UnitTest.SourceControl.Git.AbstractTest
2 | {
3 |
4 | Parameter WebAppName As STRING = "/csp/git/unittest/xsl";
5 |
6 | Property WebAppPath As %String;
7 |
8 | Method %OnNew(initvalue) As %Status
9 | {
10 | $$$QuitOnError(##super(initvalue))
11 | Kill ^SYS("SourceControl")
12 | /// add mappings for MAC and CSP
13 | Set settings = ##class(SourceControl.Git.Settings).%New()
14 | Set settings.Mappings("MAC","*")="rtn/"
15 | Set settings.Mappings("/CSP/",..#WebAppName)="csp/git/unittest/xsl"
16 | $$$ThrowOnError(settings.%Save())
17 | set ..WebAppPath = ##class(%File).TempFilename()_"d"
18 | do ##class(%File).CreateDirectoryChain(..WebAppPath)
19 | do ..CreateTestWebApp(..#WebAppName, ..WebAppPath)
20 | return $$$OK
21 | }
22 |
23 | ClassMethod CreateTestWebApp(name, path)
24 | {
25 | new $namespace
26 | set $namespace = "%SYS"
27 | kill props
28 | set props("Path") = path
29 | if '##class(Security.Applications).Exists(name) {
30 | $$$ThrowOnError(##class(Security.Applications).Create(name, .props))
31 | } else {
32 | $$$ThrowOnError(##class(Security.Applications).Create(name, .props))
33 | }
34 | }
35 |
36 | ClassMethod DeleteTestWebApp(name)
37 | {
38 |
39 | new $namespace
40 | set $namespace = "%SYS"
41 | if ##class(Security.Applications).Exists(name) {
42 | $$$ThrowOnError(##class(Security.Applications).Delete(name))
43 | }
44 | }
45 |
46 | Method %OnClose() As %Status [ Private, ServerOnly = 1 ]
47 | {
48 | do ..DeleteTestWebApp(..#WebAppName)
49 | do ##class(%File).RemoveDirectoryTree(..WebAppPath)
50 | quit ##super()
51 | }
52 |
53 | Method TestImportAll()
54 | {
55 | do ..CreateTestRoutine()
56 | $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl("test.mac"))
57 | do ..CreateStrayFileInRtn()
58 | do ..WriteFile(##class(SourceControl.Git.Settings).%New().namespaceTemp_"csp/git/unittest/xsl/test.xsl", " ")
59 | $$$ThrowOnError(##class(%Routine).Delete("test.mac"))
60 | do ##class(%RoutineMgr).Delete("/csp/git/unittest/xsl/test.xsl")
61 | $$$ThrowOnError(##class(SourceControl.Git.API).ImportAll(1))
62 | do $$$AssertTrue(##class(%Routine).Exists("test.mac"))
63 | do $$$AssertTrue(##class(%RoutineMgr).Exists("/csp/git/unittest/xsl/test.xsl"))
64 | do $$$AssertFilesSame(##class(SourceControl.Git.Settings).%New().namespaceTemp_"csp/git/unittest/xsl/test.xsl", ..WebAppPath_"/test.xsl")
65 | }
66 |
67 | Method CreateTestRoutine()
68 | {
69 | if '##class(%Routine).Exists("test.mac") {
70 | set r = ##class(%Routine).%New("test.mac")
71 | do r.WriteLine(" write 22,!")
72 | do r.Save()
73 | do r.Compile()
74 | }
75 | }
76 |
77 | /// creates a text file in the routines directory that is not really a routine
78 | Method CreateStrayFileInRtn()
79 | {
80 | set fileStream = ##class(%Stream.FileCharacter).%OpenId(
81 | ##class(%File).NormalizeFilename(
82 | "test.txt",
83 | ##class(%File).GetDirectory(##class(SourceControl.Git.Utils).FullExternalName("test.mac")))
84 | ,,.sc)
85 | $$$ThrowOnError(sc)
86 | $$$ThrowOnError(fileStream.Write("hello world!"))
87 | $$$ThrowOnError(fileStream.%Save())
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/cls/_zpkg/isc/sc/git/Favorites.cls:
--------------------------------------------------------------------------------
1 | Class %zpkg.isc.sc.git.Favorites
2 | {
3 | ClassMethod ConfigureFavoriteNamespaces(username As %String, newNamespaces As %Library.DynamicObject)
4 | {
5 | // Convert to $listbuild
6 | set namespaces = $lb()
7 | set iterator = newNamespaces.%GetIterator()
8 |
9 | while iterator.%GetNext(.key, .value) {
10 | set namespaces = namespaces_$lb(value)
11 | }
12 |
13 | // Call the private method
14 | try {
15 | do ..SetFavs(username, namespaces)
16 | } catch e {
17 | return e.AsStatus()
18 | }
19 | return $$$OK
20 | }
21 |
22 | ClassMethod GetFavoriteNamespaces(ByRef favNamespaces As %DynamicArray, ByRef nonFavNamespaces As %DynamicArray)
23 | {
24 | try {
25 | set namespaces = ..GetFavs()
26 | set favNamespaces = namespaces.%Get("Favorites")
27 | set nonFavNamespaces = namespaces.%Get("NonFavorites")
28 | } catch e {
29 | return e.AsStatus()
30 | }
31 | return $$$OK
32 | }
33 |
34 | ClassMethod GetFavs() As %Library.DynamicObject [ Private, NotInheritable ] {
35 | $$$AddAllRoleTemporary
36 | set allNamespaces = ##class(SourceControl.Git.Utils).GetContexts(1)
37 |
38 | set favNamespaces = []
39 | set nonFavNamespaces = []
40 |
41 | set username = $USERNAME
42 | set pagePrefix = "Git:"
43 | &sql(DECLARE FavCursor CURSOR FOR SELECT Page into :page from %SYS_Portal.Users where username = :username and page %STARTSWITH :pagePrefix)
44 |
45 | for i=0:1:(allNamespaces.%Size() - 1) {
46 | set namespace = allNamespaces.%Get(i)
47 | set foundFlag = 0
48 | &sql(OPEN FavCursor)
49 | throw:SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE, %msg)
50 | &sql(FETCH FavCursor)
51 | while (SQLCODE = 0) {
52 | set pageValue = "Git: "_namespace
53 | if (page = pageValue) {
54 | do favNamespaces.%Push(namespace)
55 | set foundFlag = 1
56 | }
57 | &sql(FETCH FavCursor)
58 | }
59 | &sql(CLOSE FavCursor)
60 |
61 | if ('foundFlag) {
62 | do nonFavNamespaces.%Push(namespace)
63 | }
64 | }
65 | return {"Favorites": (favNamespaces), "NonFavorites": (nonFavNamespaces)}
66 | }
67 |
68 | ClassMethod SetFavs(username As %String, namespaces As %List) [ Private, NotInheritable ] {
69 | $$$AddAllRoleTemporary
70 | &sql(DELETE FROM %SYS_Portal.Users WHERE Username = :username AND Page LIKE '%Git%')
71 |
72 | for i=1:1:$listlength(namespaces) {
73 | set namespace = $listget(namespaces, i)
74 | if (namespace '= "") {
75 | set installNamespace = namespace
76 |
77 | // Insert Git link
78 | set caption = "Git: " _ installNamespace
79 | set link = "/isc/studio/usertemplates/gitsourcecontrol/webuidriver.csp/" _ installNamespace _ "/"
80 | &sql(INSERT OR UPDATE INTO %SYS_Portal.Users (Username, Page, Data) VALUES (:username, :caption, :link))
81 |
82 | // Insert Git Pull link
83 | set caption = "Git Pull: " _ installNamespace
84 | set link = "/isc/studio/usertemplates/gitsourcecontrol/pull.csp?$NAMESPACE=" _ installNamespace
85 | &sql(INSERT OR UPDATE INTO %SYS_Portal.Users (Username, Page, Data) VALUES (:username, :caption, :link))
86 | }
87 | }
88 | }
89 | }
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/Sync.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.Sync Extends UnitTest.SourceControl.Git.AbstractTest
2 | {
3 |
4 | Method TestSync()
5 | {
6 | do $$$LogMessage("set up remote repo on filesystem")
7 | set remoteDir = ##class(%Library.File).TempFilename()_"d"
8 | if '##class(%File).CreateDirectoryChain(remoteDir,.ret) {
9 | $$$ThrowStatus($$$ERROR($$$GeneralError,"failed to create directory: "_ret))
10 | }
11 | do $zf(-100,"/SHELL","git","init",remoteDir)
12 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "config", "user.email", "unittest@example.com")
13 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "config", "user.name", "Unit Test")
14 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "checkout", "-b", "live")
15 | do ..WriteFile(remoteDir_"/cls/TestGit/SampleClass1.cls","Class TestGit.SampleClass1 {}")
16 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "add", ".")
17 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "commit", "-m", "initial commit in remote for unit test")
18 | do $$$LogMessage("initialize local repo cloning remote")
19 | $$$ThrowOnError(##class(SourceControl.Git.Utils).Clone(remoteDir_"/.git"))
20 | do $$$LogMessage("set default merge branch to live and enable basic mode")
21 | set settings = ##class(SourceControl.Git.Settings).%New()
22 | set settings.defaultMergeBranch = "live"
23 | set settings.basicMode = "system"
24 | set settings.systemBasicMode = 1
25 | $$$ThrowOnError(settings.%Save())
26 | do $$$LogMessage("check out live branch on local")
27 | $$$ThrowOnError(##class(SourceControl.Git.Utils).SwitchBranch("live"))
28 | do $$$LogMessage("create a class through IRIS, add it to source control, and sync")
29 | do $System.OBJ.Delete("TestGit.SampleClass2")
30 | set classDef = ##class(%Dictionary.ClassDefinition).%New("TestGit.SampleClass2")
31 | $$$ThrowOnError(classDef.%Save())
32 | $$$ThrowOnError($System.OBJ.Compile("TestGit.SampleClass2"))
33 | $$$ThrowOnError(##class(SourceControl.Git.Utils).AddToSourceControl("TestGit.SampleClass2.cls"))
34 | $$$ThrowOnError(##class(SourceControl.Git.Utils).Sync("should not commit"))
35 | do $$$LogMessage("sync should NOT have committed the new file since we are on the live branch.")
36 | do $$$AssertTrue(##class(SourceControl.Git.Change).IsUncommitted(##class(SourceControl.Git.Utils).FullExternalName("TestGit.SampleClass2.cls")))
37 | do $$$LogMessage("now, check out an interface branch")
38 | $$$ThrowOnError(##class(SourceControl.Git.Utils).NewBranch("interface"))
39 | do $$$LogMessage("simulate another developer's change going live")
40 | do ..WriteFile(remoteDir_"/cls/TestGit/SampleClass1.cls","Class TestGit.SampleClass1 { Parameter foo = ""bar""; }")
41 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "add", ".")
42 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "commit", "-m", "initial commit in remote for unit test")
43 | do $$$LogMessage("check out an interface branch and sync")
44 | $$$ThrowOnError(##class(SourceControl.Git.Utils).Sync("should commit"))
45 | do $$$LogMessage("sync should have rebased the other developer's change, and committed the new file.")
46 | do $$$AssertEquals(##class(TestGit.SampleClass1).#foo, "bar")
47 | do $$$AssertNotTrue(##class(SourceControl.Git.Change).IsUncommitted(##class(SourceControl.Git.Utils).FullExternalName("TestGit.SampleClass2.cls")))
48 | do $$$LogMessage("simulate a merge request on the remote from the interface branch to live.")
49 | do $zf(-100,"/SHELL","git", "-C", remoteDir, "merge", "interface")
50 | do $$$AssertTrue(##class(%File).Exists(remoteDir_"/cls/TestGit/SampleClass2.cls"))
51 | }
52 |
53 | }
54 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Settings/Document.cls:
--------------------------------------------------------------------------------
1 | /// Custom studio document type for Embedded Git settings that are controlled by a file
2 | Class SourceControl.Git.Settings.Document Extends %Studio.AbstractDocument
3 | {
4 |
5 | Projection RegisterExtension As %Projection.StudioDocument(DocumentExtension = "GSC", DocumentNew = 0, DocumentType = "json");
6 |
7 | Parameter INTERNALNAME = "embedded-git-config.GSC";
8 |
9 | Parameter EXTERNALNAME = "embedded-git-config.json";
10 |
11 | /// Return 1 if the routine 'name' exists and 0 if it does not.
12 | ClassMethod Exists(name As %String) As %Boolean
13 | {
14 | return (name = ..#INTERNALNAME)
15 | }
16 |
17 | /// Load the routine in Name into the stream Code
18 | Method Load() As %Status
19 | {
20 | set sc = $$$OK
21 | try {
22 | set stream = ..GetCurrentStream()
23 | $$$ThrowOnError(..Code.CopyFromAndSave(stream))
24 | $$$ThrowOnError(..Code.Rewind())
25 | do ..UpdateHash(stream)
26 | } catch err {
27 | set sc = err.AsStatus()
28 | }
29 | return sc
30 | }
31 |
32 | Method GetCurrentStream() As %Stream.Object
33 | {
34 | set settings = ##class(SourceControl.Git.Settings).%New()
35 | set dynObj = settings.ToDynamicObject()
36 | set formatter = ##class(%JSON.Formatter).%New()
37 | $$$ThrowOnError(formatter.FormatToStream(dynObj, .stream))
38 | return stream
39 | }
40 |
41 | /// Save the routine stored in Code
42 | Method Save() As %Status
43 | {
44 | set sc = $$$OK
45 | try {
46 | try {
47 | set settingsJSON = ##class(%DynamicObject).%FromJSON(..Code)
48 | } catch err {
49 | $$$ThrowStatus($$$ERROR($$$GeneralError, "Invalid JSON"))
50 | }
51 | set settings = ##class(SourceControl.Git.Settings).%New()
52 | do settings.ImportDynamicObject(settingsJSON)
53 | set sc = settings.%Save()
54 | quit:$$$ISERR(sc)
55 | } catch err {
56 | set sc = err.AsStatus()
57 | }
58 | return sc
59 | }
60 |
61 | ClassMethod ListExecute(ByRef qHandle As %Binary, Directory As %String, Flat As %Boolean, System As %Boolean) As %Status
62 | {
63 | if $g(Directory)'="" {
64 | set qHandle=""
65 | quit $$$OK
66 | }
67 | set qHandle = $listbuild(1,"")
68 | quit $$$OK
69 | }
70 |
71 | ClassMethod ListFetch(ByRef qHandle As %Binary, ByRef Row As %List, ByRef AtEnd As %Integer = 0) As %Status [ PlaceAfter = ListExecute ]
72 | {
73 | set Row="", AtEnd=0
74 | set rownum = $lg(qHandle,1)
75 | if rownum'=1 {
76 | set AtEnd = 1
77 | } else {
78 | set Row = $listbuild(..#INTERNALNAME,$zts-5,0,"")
79 | set $list(qHandle,1) = 2
80 | }
81 | quit $$$OK
82 | }
83 |
84 | ClassMethod ListClose(ByRef qHandle As %Binary) As %Status [ PlaceAfter = ListExecute ]
85 | {
86 | set qHandle = ""
87 | quit $$$OK
88 | }
89 |
90 | Method UpdateHash(stream)
91 | {
92 | set stream = $Get(stream,..GetCurrentStream())
93 | set hash = $System.Encryption.SHA1HashStream(stream)
94 | if $get(@##class(SourceControl.Git.Utils).#Storage@("settings","Hash")) '= hash {
95 | set @##class(SourceControl.Git.Utils).#Storage@("settings","Hash") = hash
96 | set @##class(SourceControl.Git.Utils).#Storage@("settings","TS") = $zdatetime($h,3)
97 | }
98 | }
99 |
100 | /// Return the timestamp of routine 'name' in %TimeStamp format. This is used to determine if the routine has
101 | /// been updated on the server and so needs reloading from Studio. So the format should be $zdatetime($horolog,3),
102 | /// or "" if the routine does not exist.
103 | ClassMethod TimeStamp(name As %String) As %TimeStamp
104 | {
105 | return $get(@##class(SourceControl.Git.Utils).#Storage@("settings","TS"), "")
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Log.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.Log Extends %Persistent [ Owner = {%Developer} ]
2 | {
3 |
4 | Property TimeStamp As %TimeStamp [ InitialExpression = {$zdt($h,3)} ];
5 |
6 | Property Username As %String(MAXLEN = 128) [ InitialExpression = {$Username} ];
7 |
8 | Index Username On Username [ Type = bitmap ];
9 |
10 | Property LogStream As %Stream.GlobalCharacter;
11 |
12 | Property ErrorStream As %Stream.GlobalCharacter;
13 |
14 | /// Handy as a property in case this table is mapped
15 | Property Namespace As %String [ InitialExpression = {$Namespace} ];
16 |
17 | Index Namespace On Namespace [ Type = bitmap ];
18 |
19 | Property Command As %List;
20 |
21 | ClassMethod CommandLogicalToDisplay(command As %List) As %String
22 | {
23 | If (command = "") {
24 | Quit ""
25 | }
26 | Quit "git "_$ListToString(command," ")
27 | }
28 |
29 | ClassMethod CommandBuildValueArray(command As %List, ByRef valueArray) As %Status
30 | {
31 | Set pointer = 0
32 | While $ListNext(command,pointer,element) {
33 | Set valueArray(element) = ""
34 | }
35 | Quit $$$OK
36 | }
37 |
38 | Index CommandElements On Command(KEYS) [ Type = bitmap ];
39 |
40 | Property ReturnCode As %Integer;
41 |
42 | Property Source As %String [ InitialExpression = {##class(SourceControl.Git.Log).DeriveSource()} ];
43 |
44 | Index Source On Source [ Type = bitmap ];
45 |
46 | ClassMethod Create(log As %Stream.Object = "", err As %Stream.Object = "", ByRef args, returnCode As %Integer = 0, source As %String = "")
47 | {
48 | Try {
49 | Set inst = ..%New()
50 | If $IsObject($Get(log)) {
51 | Do log.Rewind()
52 | Do inst.LogStream.CopyFromAndSave(log)
53 | Do log.Rewind()
54 | }
55 | If $IsObject($Get(err)) {
56 | Do err.Rewind()
57 | Do inst.ErrorStream.CopyFromAndSave(err)
58 | Do err.Rewind()
59 | }
60 | Set fullCommand = ""
61 | Set key = ""
62 | For {
63 | Set key = $Order(args(key),1,data)
64 | Quit:key=""
65 | Set fullCommand = fullCommand _ $ListBuild(data)
66 | }
67 | Set inst.Command = fullCommand
68 | Set inst.ReturnCode = returnCode
69 | If source '= "" {
70 | Set inst.Source = source
71 | }
72 | $$$ThrowOnError(inst.%Save())
73 | } Catch e {
74 | Do e.Log()
75 | }
76 | }
77 |
78 | ClassMethod DeriveSource() As %String
79 | {
80 | Try {
81 | If '$IsObject($Get(%request))#2 {
82 | Return "Studio"
83 | }
84 | If %request.UserAgent [ "Code/" {
85 | Return "VSCode WebView"
86 | } ElseIf (%request.UserAgent [ "node-fetch") {
87 | Return "VSCode API / Menu"
88 | } Else {
89 | Return "Management Portal"
90 | }
91 | } Catch e {
92 | Return "Unknown"
93 | }
94 | }
95 |
96 | Storage Default
97 | {
98 |
99 |
100 | %%CLASSNAME
101 |
102 |
103 | TimeStamp
104 |
105 |
106 | Username
107 |
108 |
109 | LogStream
110 |
111 |
112 | ErrorStream
113 |
114 |
115 | Namespace
116 |
117 |
118 | Command
119 |
120 |
121 | ReturnCode
122 |
123 |
124 | Source
125 |
126 |
127 | ^SourceControl.Git.LogD
128 | LogDefaultData
129 | ^SourceControl.Git.LogD
130 | ^SourceControl.Git.LogI
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | ^SourceControl.Git.LogS
143 | %Storage.Persistent
144 | }
145 |
146 | }
147 |
148 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/API.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.API
2 | {
3 |
4 | /// Configures settings for Git integration
5 | ClassMethod Configure()
6 | {
7 | set sc = $$$OK
8 | set initTLevel = $tlevel
9 | try {
10 | tstart
11 | $$$ThrowOnError(##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension"))
12 | write !,"Configured SourceControl.Git.Extension as source control class for namespace ",$namespace
13 | set mappingsNode = ##class(SourceControl.Git.Utils).MappingsNode()
14 | if '$data(@mappingsNode) {
15 | do ##class(SourceControl.Git.Utils).SetDefaultMappings(mappingsNode)
16 | write !,"Configured default mappings for classes, routines, and include files. You can customize these in the global:",!?5,mappingsNode
17 | }
18 | set gitExists = ##class(SourceControl.Git.Utils).GitBinExists(.version)
19 | set gitBinPath = ##class(SourceControl.Git.Utils).GitBinPath(.isDefault)
20 |
21 | if gitExists && isDefault {
22 | // Note: version starts with "git version"
23 | write !,version," is available via PATH. You may enter a path to a different version if needed."
24 | }
25 | set good = ##class(SourceControl.Git.Settings).Configure()
26 | if 'good {
27 | write !,"Cancelled."
28 | quit
29 | }
30 | tcommit
31 | } catch e {
32 | set sc = e.AsStatus()
33 | write !,$system.Status.GetErrorText(sc)
34 | }
35 | while $tlevel > initTLevel {
36 | trollback 1
37 | }
38 | }
39 |
40 | /// API for git pull - just wraps Utils
41 | /// - pTerminateOnError: if set to 1, this will terminate on error if there are any errors in the pull, otherwise will return status
42 | ClassMethod Pull(pTerminateOnError As %Boolean = 0)
43 | {
44 | set st = ##class(SourceControl.Git.Utils).Pull(,pTerminateOnError)
45 | if pTerminateOnError && $$$ISERR(st) {
46 | Do $System.Process.Terminate($Job,1)
47 | }
48 | quit st
49 | }
50 |
51 | /// Imports all items from the Git repository into IRIS.
52 | /// - pForce: if true, will import an item even if the last updated timestamp in IRIS is later than that of the file on disk.
53 | ClassMethod ImportAll(pForce As %Boolean = 0) as %Status
54 | {
55 | return ##class(SourceControl.Git.Utils).ImportAll(pForce)
56 | }
57 |
58 | /// Locks the environment to prevent changes to code other than through git pull.
59 | /// Returns 1 if the environment was already locked, 0 if it was previously unlocked.
60 | ClassMethod Lock()
61 | {
62 | quit ##class(SourceControl.Git.Utils).Locked(1)
63 | }
64 |
65 | /// Unlocks the environment to allow changes through the IDE.
66 | /// Returns 1 if the environment was already locked, 0 if it was previously unlocked.
67 | ClassMethod Unlock()
68 | {
69 | quit ##class(SourceControl.Git.Utils).Locked(0)
70 | }
71 |
72 | /// Run in terminal to baseline a namespace by adding all items to source control.
73 | /// - pCommitMessage: if defined, all changes in namespace context will be committed.
74 | /// - pPushToRemote: if defined, will run a git push to the specified remote
75 | ClassMethod BaselineExport(pCommitMessage = "", pPushToRemote = "") As %Status
76 | {
77 | quit ##class(SourceControl.Git.Utils).BaselineExport(pCommitMessage, pPushToRemote)
78 | }
79 |
80 | ClassMethod MapEverywhere() As %Status
81 | {
82 | Quit ##class(SourceControl.Git.Installer).MapEverywhere()
83 | }
84 |
85 | /// Run to baseline all interoperability productions in the namespace to source control.
86 | /// This should be done after changing the value of the "decompose productions" setting.
87 | ClassMethod BaselineProductions()
88 | {
89 | do ##class(SourceControl.Git.Util.Production).BaselineProductions()
90 | }
91 |
92 | /// Given the path to a directory that contains production items, this method will import them all
93 | /// and delete any custom items from the production configuration that do not exist in the directory.
94 | /// This method may be called on a namespace that is not configured with Embedded Git for source control.
95 | ClassMethod LoadProductionsFromDirectory(pDirectoryName, Output pFailedItems) As %Status
96 | {
97 | return ##class(SourceControl.Git.Util.Production).LoadProductionsFromDirectory(pDirectoryName, .pFailedItems)
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
124 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/folder.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
124 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/Util/ResolutionManager.cls:
--------------------------------------------------------------------------------
1 | Include (%occInclude, %occErrors, %occKeyword, %occReference, %occSAX)
2 |
3 | Class SourceControl.Git.Util.ResolutionManager Extends %RegisteredObject
4 | {
5 |
6 | Property logStream As %Stream.Object [ Private ];
7 |
8 | Property errorStatus As %Status [ InitialExpression = 1, Private ];
9 |
10 | /// API property: whether or not the conflict was resolved
11 | Property resolved As %Boolean [ InitialExpression = 0 ];
12 |
13 | /// API property: error message if resolved is false
14 | Property errorMessage As %String [ Calculated ];
15 |
16 | Method errorMessageGet() As %String
17 | {
18 | If $$$ISERR(..errorStatus) {
19 | Do $System.Status.DecomposeStatus(..errorStatus,.components)
20 | If $Get(components(1,"code")) = $$$GeneralError {
21 | Quit $Get(components(1,"param",1))
22 | } Else {
23 | Set ex = ##class(%Exception.StatusException).CreateFromStatus(..errorStatus)
24 | Do ex.Log()
25 | Quit "an internal error occurred and has been logged."
26 | }
27 | } Else {
28 | Quit ""
29 | }
30 | }
31 |
32 | ClassMethod FromLog(pOutStream As %Stream.Object) As SourceControl.Git.Util.ProductionConflictResolver
33 | {
34 | Set inst = ..%New()
35 | Try {
36 | Set inst.logStream = pOutStream
37 | Do inst.ConsumeStream()
38 | } Catch e {
39 | Set inst.resolved = 0
40 | Set inst.errorStatus = e.AsStatus()
41 | }
42 | Do inst.logStream.Rewind() // Finally
43 | Quit inst
44 | }
45 |
46 | Method ConsumeStream() [ Private ]
47 | {
48 | Set conflicts = 0
49 | Do ..logStream.Rewind()
50 | Do ..logStream.ReadLine()
51 | while '..logStream.AtEnd {
52 | Set conflictLine = ..logStream.ReadLine()
53 | If $Extract(conflictLine,1,8) = "CONFLICT" {
54 | Set conflicts($i(conflicts)) = $Piece(conflictLine,"Merge conflict in ",2)
55 | }
56 | }
57 | If (conflicts = 0) {
58 | $$$ThrowStatus($$$ERROR($$$GeneralError,"Message did not reflect merge conflict on a single file."))
59 | }
60 | For i=1:1:conflicts {
61 | Set targetFile = conflicts(i)
62 | Write !,"Attempting intelligent auto-merge for: "_targetFile
63 | Set internalName = ##class(SourceControl.Git.Utils).NameToInternalName(targetFile)
64 | If ($Piece(internalName,".",*) '= "CLS") {
65 | $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict is not a class."))
66 | }
67 |
68 | Set targetClass = $Piece(internalName,".",1,*-1)
69 | If '$$$comClassDefined(targetClass) {
70 | $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict not a known class."))
71 | }
72 |
73 | Set resolverClass = $Select(
74 | $classmethod(targetClass,"%Extends","Ens.Production"):"SourceControl.Git.Util.ProductionConflictResolver",
75 | $classmethod(targetClass,"%Extends","Ens.Rule.Definition"):"SourceControl.Git.Util.RuleConflictResolver",
76 | 1:""
77 | )
78 |
79 | If (resolverClass = "") {
80 | $$$ThrowStatus($$$ERROR($$$GeneralError,"File with conflict not a class type that supports automatic resolution."))
81 | }
82 |
83 | do ..ResolveClass(targetClass, targetFile, resolverClass)
84 |
85 | set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "add", targetFile)
86 | if (code '= 0) {
87 | $$$ThrowStatus($$$ERROR($$$GeneralError,"git add reported failure"))
88 | }
89 | }
90 |
91 | set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "commit", "--no-edit")
92 | if (code '= 0) {
93 | $$$ThrowStatus($$$ERROR($$$GeneralError,"git commit reported failure"))
94 | }
95 |
96 | set code = ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "rebase", "--continue")
97 | if (code '= 0) {
98 | // Could hit a second+ conflict in the same rebase; attempt to resolve the next one too.
99 | set resolver = ..FromLog(outStream)
100 | set ..resolved = resolver.resolved
101 | set ..errorStatus = resolver.errorStatus
102 | } else {
103 | set ..resolved = 1
104 | }
105 | }
106 |
107 | Method ResolveClass(className As %String, fileName As %String, resolverClass As %Dictionary.Classname) [ Private ]
108 | {
109 | Set filePath = ##class(SourceControl.Git.Utils).TempFolder()_fileName
110 | Set file = ##class(%Stream.FileCharacter).%OpenId(filePath,,.sc)
111 | $$$ThrowOnError(sc)
112 |
113 | Set resolver = $classmethod(resolverClass,"%New")
114 | Do resolver.ResolveStream(file) // Throws exception on failure
115 |
116 | $$$ThrowOnError(##class(SourceControl.Git.Utils).ImportItem(className_".CLS",1))
117 | $$$ThrowOnError($System.OBJ.Compile(className,"ck"))
118 | }
119 |
120 | }
121 |
122 |
--------------------------------------------------------------------------------
/docs/testing.md:
--------------------------------------------------------------------------------
1 | # git-source-control Testing Plan
2 |
3 | The following is a testing plan that should be followed prior to release of a new version.
4 |
5 | - Using a IRIS user with %All permissions, run `##class(SourceControl.Git.API).Configure()` in a terminal on a fresh namespace. Terminal prompts should describe each setting. Create an SSH key and use it to clone a remote repository.
6 | - The following steps should be run with an IRIS user with %Developer role (not %All).
7 | - Use VS Code to create a new class. Use the Source Control menu to Import All from the repository. Check the output to confirm that the contents of the repository were imported and compiled.
8 | - Test changing Git project settings in a web browser and in Studio. Input labels and tooltips should describe each setting.
9 | - In Expert Mode, test:
10 | - Add a new item through Studio / VS Code. View the Source Control Menu - there should be an option to remove it from source control. The item should show up in the Workspace view of the WebUI.
11 | - Stash the item in the WebUI. It should be deleted from IRIS. Pop it from the stash. It should be imported and compiled. Discard the item in the WebUI. It should be deleted from IRIS.
12 | - Add, delete, and modify some items in Studio / VS Code. Commit through the WebUI with a commit message and details. The commit should show with the expected commit message and differences in the branch view.
13 | - Select the branch in the branch view and click "Push Branch". It should successfully push changes to the remote repository.
14 | - Create a new local branch. Add a new item and commit it. Switch between the new branch and the old branch. The item should be added and deleted from IRIS. Test merging the new branch to the old branch in the web UI. The item should be added to IRIS.
15 | - Edit a file on the remote repository. Use the "Git Pull" link from the System Management Portal favorites to pull. The preview should show the change without actually pulling it or loading it into IRIS. Confirming should do the pull and load the change into IRIS.
16 | - Edit an item through Studio / VS Code. Log in with a different IRIS user and attempt to edit the same item. The edit should be prohibited. Open the WebUI. The workspace view should list that item and indicate that it is checked out by another user. Stash the item, then try to edit it again. This time the edit should succeed.
17 | - In Basic Mode, test:
18 | - Add, edit, and delete items through Studio / VS Code. Use the Sync option. All changes should be committed and pushed to the remote.
19 | - 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.
20 | - 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.
21 | - Make sure production decomposition is off. 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.
22 |
23 | ## Testing production decomposition
24 | - Enable production decomposition in the git-source-control settings.
25 | - 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.
26 | - 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.
27 | - Revert some production items through the workspace view in the Web UI. The production should automatically update.
28 | - In Basic mode, test deployment:
29 | - 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.
30 | - 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.
31 | - Test migration of a production to decomposed format:
32 | - 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.
33 | - On the second namespace, sync and confirm the new production has been created.
34 | - On the initial namespace, turn on production decomposition. Open the production page and use the "Export Production" option in the source control menu. 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.
35 | - On the second namespace, turn on production decomposition. Sync. The production should be reloaded with no changes.
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/computer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
148 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/File.cls:
--------------------------------------------------------------------------------
1 | /// Has a cache of file internal/external name mappings as of LastModifiedTime.
2 | Class SourceControl.Git.File Extends %Persistent
3 | {
4 |
5 | Property ExternalName As %String(MAXLEN = "") [ Required ];
6 |
7 | Property ExternalNameHash As %String [ Calculated, SqlComputeCode = {set {*} = $System.Encryption.SHAHash(256,{ExternalName})}, SqlComputed ];
8 |
9 | Property InternalName As %String(MAXLEN = 255) [ Required ];
10 |
11 | Property LastModifiedTime As %String [ Required ];
12 |
13 | Index InternalName On InternalName;
14 |
15 | Index ExternalNameHash On ExternalNameHash [ Unique ];
16 |
17 | ClassMethod ExternalNameToInternalName(ExternalName As %String) As %String
18 | {
19 | set internalName = ""
20 | if ##class(%File).Exists(ExternalName) {
21 | set lastModified = ##class(%Library.File).GetFileDateModified(ExternalName)
22 | set hash = $System.Encryption.SHAHash(256,ExternalName)
23 | if ..ExternalNameHashExists(hash,.id) {
24 | set inst = ..%OpenId(id,,.sc)
25 | $$$ThrowOnError(sc)
26 | if inst.LastModifiedTime = lastModified {
27 | quit inst.InternalName
28 | } else {
29 | set inst.LastModifiedTime = lastModified
30 | }
31 | } else {
32 | set inst = ..%New()
33 | set inst.ExternalName = ExternalName
34 | set inst.LastModifiedTime = lastModified
35 | }
36 | new %SourceControl //don't trigger source hooks with this test load to get the Name
37 | set sc=$system.OBJ.Load(ExternalName,"-d",,.outName,1)
38 | // If the test load was unsuccessful then it may be due to an unsupported
39 | // file type (e.g. hl7 or lut) that we may otherwise be able to handle
40 | if $$$ISERR(sc) {
41 | set outName = ..ParseFileForInternalName(ExternalName)
42 | }
43 | set itemIsPTD = 0
44 | if $data(outName) = 11 {
45 | set key = $order(outName(""))
46 | while (key '= "") {
47 | if ($zconvert($piece(outName,".",*),"U") = "PTD") {
48 | set itemIsPTD = 1
49 | quit
50 | }
51 | set key = $order(outName(key))
52 | }
53 | }
54 | if itemIsPTD && ##class(%Library.EnsembleMgr).IsEnsembleNamespace() {
55 | do ##class(SourceControl.Git.Production).ParseExternalName($replace(ExternalName,"\","/"),.internalName)
56 | } elseif (($data(outName)=1) || ($data(outName) = 11 && ($order(outName(""),-1) = $order(outName(""))))) && ($zconvert(##class(SourceControl.Git.Utils).Type(outName),"U") '= "CSP") {
57 | set internalName = outName
58 | }
59 | if (internalName '= "") {
60 | set inst.InternalName = internalName
61 | $$$ThrowOnError(inst.%Save())
62 | }
63 | }
64 | quit internalName
65 | }
66 |
67 | /// Attempt to determine the internal name of a given file based on its content
68 | /// Intended to be used in situations where $system.OBJ.Load is unable to
69 | ClassMethod ParseFileForInternalName(fileName As %String) As %String [ Private ]
70 | {
71 | Set internalName = ""
72 |
73 | Set fileExtension = $ZCONVERT($PIECE(fileName,".",*),"U")
74 | If (fileExtension = "HL7") {
75 | Set tSC = ##class(%XML.TextReader).ParseFile(fileName, .textReader)
76 | If ($$$ISOK(tSC)) {
77 | // The HL7 schema name is in the 'name' attribute of the 'Category' element
78 | // Example:
79 | If (textReader.ReadStartElement("Category") && textReader.MoveToAttributeName("name")) {
80 | If (textReader.Value '= "") {
81 | Set internalName = textReader.Value_"."_fileExtension
82 | }
83 | }
84 | }
85 | } ElseIf (fileExtension = "LUT") {
86 | Set tSC = ##class(%XML.TextReader).ParseFile(fileName, .textReader)
87 | If $$$ISOK(tSC) {
88 | // The lookup table name is in the 'table' attribute of any 'entry' element
89 | // Example:
90 | If (textReader.ReadStartElement("entry") && textReader.MoveToAttributeName("table")) {
91 | If (textReader.Value '= "") {
92 | Set internalName = textReader.Value_"."_fileExtension
93 | }
94 | }
95 | }
96 | }
97 |
98 | Quit internalName
99 | }
100 |
101 | Storage Default
102 | {
103 |
104 |
105 | %%CLASSNAME
106 |
107 |
108 | ExternalName
109 |
110 |
111 | InternalName
112 |
113 |
114 | LastModifiedTime
115 |
116 |
117 | ^SourceControl.Git.FileD
118 | FileDefaultData
119 | ^SourceControl.Git.FileD
120 | ^SourceControl.Git.FileI
121 | ^SourceControl.Git.FileS
122 | %Storage.Persistent
123 | }
124 |
125 | }
126 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/computer.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
148 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/file.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
134 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/file.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
134 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
141 |
--------------------------------------------------------------------------------
/git-webui/release/share/git-webui/webui/img/star.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
141 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/DiscardState.cls:
--------------------------------------------------------------------------------
1 | Class SourceControl.Git.DiscardState Extends (%Persistent, %JSON.Adaptor)
2 | {
3 |
4 | Property FullExternalName As %String(MAXLEN = "") [ Required ];
5 |
6 | Property Name As %String [ Required ];
7 |
8 | Property Contents As %Stream.GlobalCharacter(LOCATION = "^SourceControl.Git.DiscardS");
9 |
10 | Property Username As %String [ Required ];
11 |
12 | Property Branch As %String [ Required ];
13 |
14 | Property Timestamp As %TimeStamp [ Required ];
15 |
16 | Property ExternalFile As %Boolean [ Required ];
17 |
18 | /// Boolean tracking whether or not file was deleted as part of change
19 | Property Deleted As %Boolean;
20 |
21 | Index BranchMap On Branch [ Type = bitmap ];
22 |
23 | Method RestoreToFileTree()
24 | {
25 | // Make sure directory for file exists
26 | set dir = ##class(%File).GetDirectory(..FullExternalName)
27 | if ('##class(%File).DirectoryExists(dir)) {
28 | do ##class(%File).CreateDirectoryChain(dir)
29 | }
30 |
31 | if (..Deleted) {
32 | do ##class(%File).Delete(..FullExternalName)
33 | } else {
34 |
35 | // Recreate File
36 | set fileStream = ##class(%Stream.FileCharacter).%New()
37 | set fileStream.Filename = ..FullExternalName
38 | $$$ThrowOnError(fileStream.CopyFrom(..Contents))
39 | $$$ThrowOnError(fileStream.%Save())
40 |
41 | // Add file to source-control / IRIS
42 | if '..ExternalFile {
43 | do ##class(SourceControl.Git.Utils).ImportItem(..Name, 1, 1, 1)
44 | do ##class(SourceControl.Git.Utils).AddToServerSideSourceControl(..Name)
45 | }
46 | }
47 |
48 | // Delete discard record
49 | $$$ThrowOnError(..%DeleteId(..%Id()))
50 | }
51 |
52 | ClassMethod SaveDiscardState(InternalName As %String, name As %String) As %Status
53 | {
54 | set discardState = ..%New()
55 |
56 | if (InternalName = "") {
57 | // If not in IRIS
58 | set externalName = ##class(%File).Construct(##class(SourceControl.Git.Utils).DefaultTempFolder(),name)
59 | set discardState.FullExternalName = externalName
60 | set discardState.Name = name
61 | set discardState.ExternalFile = 1
62 | } else {
63 | set discardState.FullExternalName = ##class(SourceControl.Git.Utils).FullExternalName(InternalName)
64 | set discardState.Name = InternalName
65 | set discardState.ExternalFile = 0
66 | }
67 | // Copy over file contents
68 | if (##class(%File).Exists(discardState.FullExternalName)) {
69 | set fileStream = ##class(%Stream.FileCharacter).%New()
70 | set fileStream.Filename = discardState.FullExternalName
71 | do fileStream.%Open()
72 | do discardState.Contents.CopyFrom(fileStream)
73 | do fileStream.%Close()
74 | } else {
75 | set discardState.Deleted = 1
76 | do discardState.Contents.Write("Deleted File")
77 | }
78 |
79 | // Save extra information
80 | set discardState.Username = $USERNAME
81 | set discardState.Branch = ##class(SourceControl.Git.Utils).GetCurrentBranch()
82 | set discardState.Timestamp = $zdatetime($horolog, 3)
83 |
84 | set st = discardState.%Save()
85 |
86 | quit st
87 | }
88 |
89 | ClassMethod DiscardStatesInBranch() As %DynamicArray
90 | {
91 | set currentBranch = ##class(SourceControl.Git.Utils).GetCurrentBranch()
92 |
93 | // Use embedded SQL for backwards compatability
94 | &sql(DECLARE DiscardCursor CURSOR FOR SELECT ID into :id from SourceControl_Git.DiscardState WHERE branch = :currentBranch)
95 | &sql(OPEN DiscardCursor)
96 | throw:SQLCODE<0 ##class(%Exception.SQL).CreateFromSQLCODE(SQLCODE, %msg)
97 | &sql(FETCH DiscardCursor)
98 | set discardStates = []
99 | while(SQLCODE = 0) {
100 | set discardState = ..%OpenId(id)
101 | do discardState.%JSONExportToString(.JSONStr)
102 | set discardStateObject = ##class(%DynamicAbstractObject).%FromJSON(JSONStr)
103 | set discardStateObject.Id = id
104 | do discardStates.%Push(discardStateObject)
105 | &sql(FETCH DiscardCursor)
106 | }
107 | &sql(CLOSE DiscardCursor)
108 |
109 | quit discardStates
110 | }
111 |
112 | Storage Default
113 | {
114 |
115 |
116 | %%CLASSNAME
117 |
118 |
119 | FullExternalName
120 |
121 |
122 | InternalName
123 |
124 |
125 | Contents
126 |
127 |
128 | Username
129 |
130 |
131 | Branch
132 |
133 |
134 | Timestamp
135 |
136 |
137 | Name
138 |
139 |
140 | ExternalFile
141 |
142 |
143 | Deleted
144 |
145 |
146 | ^SourceControl22B9.DiscardStateD
147 | DiscardStateDefaultData
148 | ^SourceControl22B9.DiscardStateD
149 | ^SourceControl22B9.DiscardStateI
150 | ^SourceControl22B9.DiscardStateS
151 | %Storage.Persistent
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/docs/menu-items.md:
--------------------------------------------------------------------------------
1 | # git-source-control Menu Items
2 |
3 | ## Status
4 | This menu option is analogous to the [git status](https://git-scm.com/docs/git-status) command and prints the status of the repository to the output.
5 |
6 |
7 | ## Settings
8 | This option opens the GUI's settings page project specific git-source-control settings can be configured. This includes the settings that were configured when running:
9 | ```
10 | do ##class(SourceControl.Git.API).Configure()
11 | ```
12 |
13 | This page also includes the mappings configurations.
14 |
15 | Any changes made to the settings must be saved using the 'Save' button in order to take effect.
16 |
17 |
18 | ## Launch Git UI
19 | This menu option opens the git-source-control GUI. From here commit messages can be written, files can be staged and committed, branches can be viewed.
20 |
21 |
22 | ## Add
23 | This menu option is analogous to the [git add](https://git-scm.com/docs/git-add) command. It will perform 'git add' on the currently open file, adding it to the files that can be staged.
24 |
25 |
26 | ## Remove
27 | This menu option will only appear if the currently open file has been already added using the 'Add' menu option. It undoes the effect of adding the file, similar to running [git reset](https://git-scm.com/docs/git-reset) on a specific file.
28 |
29 |
30 | ## Push to Remote Branch
31 | This option pushes the commits in the branch to the remote repository. This exhibits the same behavior as the [git push](https://git-scm.com/docs/git-push) command.
32 |
33 |
34 | ## Push to Remote Branch (force)
35 | This option forcibly pushes the commits in the branch to the remote repository. This is potentially destructive and may overwrite the commit history of the remote branch. This exhibits the same behavior as the [git push --force](https://git-scm.com/docs/git-push) command.
36 |
37 |
38 | ## Fetch from Remote
39 | This option first [fetches](https://git-scm.com/docs/git-fetch) the most recent version of the branch without merging that version into the local copy of the branch. It will then list all files modified between the current version and the remote version.
40 |
41 | This also has the effect of refreshing the list of all remote branches and pruning any references that no longer exist in the remote. (see: [git fetch --prune](https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---prune))
42 |
43 |
44 | ## Pull Changes from Remote Branch
45 | Much like the [git pull](https://git-scm.com/docs/git-pull) command, this menu option pulls the most recent version of the current branch from a remote source, merging the changes into the local copy.
46 |
47 |
48 | ## Sync
49 | This option will synchronize the current branch checked out a local repo with the same branch in a remote repo. It encapsulates the pattern of fetching, pulling, committing, and pushing into one menu action.
50 | - If you are on the Default Merge Branch, then Sync only pulls the latest commits from the remote. Committing is disallowed on the Default Merge Branch.
51 | - If there is no defined remote repository, it will simply commit all staged files.
52 | - If there is a Default Merge Branch defined, then sync attempts to perform a [rebase](https://git-scm.com/docs/git-rebase) onto the latest Default Merge Branch from the remote.
53 | - If the rebase were to result in merge conflicts, then this action is aborted so the system is not left in an inconsistent state.
54 |
55 | The sync operation is only enabled in basic mode.
56 |
57 |
58 | ## Create New Branch
59 | This menu option creates a new branch in the repository for changes to be committed to. It also changes the current branch to be the created branch. This mimics the behavior of the [git checkout -b](https://git-scm.com/docs/git-checkout) command.
60 |
61 | In basic mode, this option first checks out the Default Merge Branch (if defined) and pulls that branch from the remote before creating the new branch.
62 |
63 |
64 | ## Check Out an Existing Branch
65 | This option refreshes the local list of branches available in the upstream repository, and then changes the currently checked out branch to the provided branch. This mimics the behavior of the [git fetch --prune](https://git-scm.com/docs/git-fetch#Documentation/git-fetch.txt---prune) and [git checkout](https://git-scm.com/docs/git-checkout) commands.
66 |
67 | If the desired branch does not exist in your local or in the remote, then you will receive the "Selected branch does not exist" error message.
68 |
69 | ## Export System Default Settings
70 | This option will export interoperability [system default settings](https://docs.intersystems.com/irislatest/csp/docbook/DocBook.UI.Page.cls?KEY=ECONFIG_other_default_settings) to the Git repository. Only system default settings marked as "deployable" will be exported. The Embedded Git settings must be configured with a mapping for items of type ESD to a location in the repository for system default settings to export.
71 |
72 |
73 | ## Export All
74 | This option exports class files to the local file tree at the configured location.
75 |
76 |
77 | ## Export All (Force)
78 | This option exports all class files regardless of whether they're already up to date in the local file tree or not.
79 |
80 |
81 | ## Import All
82 | This option imports the versions of the files that are found in the configured directory into the project. Files that are out of date or the same as the files in the project won't be imported.
83 |
84 |
85 | ## Import All (Force)
86 | This menu option behaves similarly to the regular import but forces the files to be imported regardless of whether the on-disk version is the same or older.
87 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the workflow will run
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the "main" branch
8 | [push, pull_request]
9 |
10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
11 | jobs:
12 | # This workflow contains a single job called "build"
13 | build:
14 | # The type of runner that the job will run on
15 | runs-on: ubuntu-latest
16 |
17 | env:
18 | #Environment variables usable throughout the "build" job, e.g. in OS-level commands
19 |
20 | # ** FOR GENERAL USE, LIKELY NEED TO CHANGE **
21 | package: git-source-control
22 | container_image: intersystemsdc/iris-community:latest
23 |
24 | # ** FOR GENERAL USE, MAY NEED TO CHANGE **
25 | build_flags: -dev -verbose
26 | test_package: UnitTest
27 |
28 | # ** FOR GENERAL USE, SHOULD NOT NEED TO CHANGE **
29 | instance: iris
30 | artifact_dir: build-artifacts
31 |
32 | # Note: test_reports value is duplicated in test_flags environment variable
33 | test_reports: test-reports
34 | test_flags: >-
35 | -verbose -DUnitTest.ManagerClass=TestCoverage.Manager -DUnitTest.JUnitOutput=/test-reports/junit.xml
36 | -DUnitTest.FailuresAreFatal=1 -DUnitTest.Manager=TestCoverage.Manager
37 | -DUnitTest.UserParam.CoverageReportClass=TestCoverage.Report.Cobertura.ReportGenerator
38 | -DUnitTest.UserParam.CoverageReportFile=/source/coverage.xml
39 |
40 | # Steps represent a sequence of tasks that will be executed as part of the job
41 | steps:
42 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
43 | - uses: actions/checkout@v3
44 |
45 | - name: Run Container
46 | run: |
47 | # Create test_reports directory to share test results before running container
48 | mkdir $test_reports
49 | chmod 777 $test_reports
50 |
51 | # Same for artifact directory
52 | mkdir $artifact_dir
53 | chmod 777 $artifact_dir
54 |
55 | # Run InterSystems IRIS Instance
56 | docker pull $container_image
57 | docker run -d -h $instance --name $instance -v $GITHUB_WORKSPACE:/source:rw -v $GITHUB_WORKSPACE/$test_reports:/$test_reports:rw -v $GITHUB_WORKSPACE/$artifact_dir:/$artifact_dir:rw --init $container_image
58 | echo halt > wait
59 | # Wait for instance to be ready
60 | until docker exec --interactive $instance iris session $instance < wait; do sleep 1; done
61 |
62 | - name: Install TestCoverage
63 | run: |
64 | echo "zpm \"install testcoverage\":1:1" > install-testcoverage
65 | docker exec --interactive $instance iris session $instance -B < install-testcoverage
66 | chmod 777 $GITHUB_WORKSPACE
67 |
68 | - name: Build and Test
69 | run: |
70 | # Run build
71 | echo "zpm \"load /source $build_flags\":1:1" > build.script
72 | # Test package is compiled first as a workaround for some dependency issues.
73 | echo "do \$System.OBJ.CompilePackage(\"$test_package\",\"ckd\") " > test.script
74 | # Run tests
75 | echo "zpm \"$package test -only $test_flags\":1:1" >> test.script
76 | docker exec --interactive $instance iris session $instance -B < build.script && docker exec --interactive $instance iris session $instance -B < test.script
77 |
78 | - name: Upload coverage reports to Codecov with GitHub Action
79 | uses: codecov/codecov-action@v4.2.0
80 | env:
81 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
82 |
83 | - name: Produce CE Artifact
84 | run: |
85 | echo "set version=##class(%IPM.Storage.Module).NameOpen(\"git-source-control\").VersionString" > package.script
86 | echo "set file=\"/$artifact_dir/git-source-control-\"_version_\".xml\" write !,file,!" >> package.script
87 | echo "do ##class(SourceControl.Git.Utils).BuildCEInstallationPackage(file)" >> package.script
88 | echo "halt" >> package.script
89 | docker exec --interactive $instance iris session $instance -B < package.script
90 | echo $GITHUB_WORKSPACE/$artifact_dir
91 | ls $GITHUB_WORKSPACE/$artifact_dir
92 |
93 | - name: Attach CE Artifact
94 | uses: actions/upload-artifact@v4
95 | if: always()
96 | with:
97 | name: "PreIRISInstallationPackage"
98 | path: ${{ github.workspace }}/${{ env.artifact_dir }}/*.xml
99 | if-no-files-found: error
100 |
101 | - name: XUnit Viewer
102 | id: xunit-viewer
103 | uses: AutoModality/action-xunit-viewer@v1
104 | if: always()
105 | with:
106 | # With -DUnitTest.FailuresAreFatal=1 a failed unit test will fail the build before this point.
107 | # This action would otherwise misinterpret our xUnit style output and fail the build even if
108 | # all tests passed.
109 | fail: false
110 |
111 | - name: Attach the report
112 | uses: actions/upload-artifact@v4
113 | if: always()
114 | with:
115 | name: ${{ steps.xunit-viewer.outputs.report-name }}
116 | path: ${{ steps.xunit-viewer.outputs.report-dir }}
117 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/NameTest.cls:
--------------------------------------------------------------------------------
1 | Import SourceControl.Git
2 |
3 | Include SourceControl.Git
4 |
5 | Class UnitTest.SourceControl.Git.NameTest Extends %UnitTest.TestCase
6 | {
7 |
8 | Property Mappings [ MultiDimensional ];
9 |
10 | Method TestNoExtension()
11 | {
12 | // This method will test a case where the passed filename has no extension.
13 | // We should return an empty string in this case.
14 | do $$$AssertEquals(##class(Utils).Name("Test"),"")
15 | }
16 |
17 | Method TestNonExistantMappings()
18 | {
19 |
20 | // This method will test cases where no mapping exists for some or all files of a certain file type.
21 | // Default behaviour should be followed here - the path should be "/"
22 | // Example: For "ABC.def.ghi.xzy", the output should be "xzy/ABC.def.ghi.xzy"
23 |
24 | // No mapping
25 | do $$$AssertEquals(##class(Utils).Name("Name.OtherName.OtherMoreDifferentName.xzy"),"xzy/Name.OtherName.OtherMoreDifferentName.xzy")
26 | // Mappings for some files
27 | do $$$AssertEquals(##class(Utils).Name("Name.OtherName.OtherMoreDifferentName.acb"),"acb/Name.OtherName.OtherMoreDifferentName.acb")
28 | // Regular class that doesn't exist and we don't ignore non-existent classes
29 | do $$$AssertEquals(##class(Utils).Name("XZYName.OtherName.OtherMoreDifferentName.acb"),"acb/XZYName/OtherName/OtherMoreDifferentName.acb")
30 | }
31 |
32 | Method TestBasicMappings()
33 | {
34 |
35 | // This method will test cases where a mapping exists for all files of a certain file type with foldering enable.
36 | // This is the most simple usecase for the Name() method.
37 |
38 | // File corresponding to a universal mapping with foldering enabled
39 | do $$$AssertEquals(##class(Utils).Name("SourceControl.Git.Utils.cls"),"cls/SourceControl/Git/Utils.cls")
40 | // File corresponding to a specific mapping with foldering enabled
41 | do $$$AssertEquals(##class(Utils).Name("UnitTest.SourceControl.Git.NameTest.cls"),"test/UnitTest/SourceControl/Git/NameTest.cls")
42 | // File corresponding to a universal mapping with special handling
43 | do $$$AssertEquals(##class(Utils).Name("test2.pivot.dfi"),"test/_resources/dfi/test2.pivot.dfi")
44 | }
45 |
46 | Method TestOnlyNoFolders()
47 | {
48 | // This method will test cases where a mapping exists for all files of a certain file type with foldering disabled.
49 | do $$$AssertEquals(##class(Utils).Name("Name.OtherName.OtherMoreDifferentName.nf"),"nf/sf/Name.OtherName.OtherMoreDifferentName.nf")
50 | }
51 |
52 | Method TestMixedFoldering()
53 | {
54 | // This method will test cases where multiple mappings exist a file type with mix od foldering enabled and disabled.
55 | // There are 3 cases here.
56 | // 1. Foldering is enabled for the universal mapping but is disabled for some packages.
57 | // 2. Foldering is disabled for the universal mapping but is enabled for some packages.
58 | // 3. There is no specified universal mapping, so default behaviour (no foldering) should apply. But there are specific mappings for certain packages.
59 | // 3 is covered in a previous test.
60 |
61 | // 1
62 | do $$$AssertEquals(##class(Utils).Name("TestPackage.Hello.World.inc"),"inc/TestPackage.Hello.World.inc")
63 | // 2
64 | do $$$AssertEquals(##class(Utils).Name("TestPackage.Hello.World.mac"),"rtn/TestPackage/Hello/World.mac")
65 | }
66 |
67 | Method TestParamExpansion()
68 | {
69 | try {
70 | set $$$SourceMapping("ESD","*") = "config//"
71 | set $$$SourceMapping("ESD","*","NoFolders") = 1
72 | set $$$SourceMapping("CLS","*") = "/cls/"
73 | set $$$SourceMapping("INC","*") = "/inc/"
74 | set settings = ##class(SourceControl.Git.Settings).%New()
75 | set oldEnvName = settings.environmentName
76 | set settings.environmentName = "TEST"
77 | set settings.mappingsToken = "mdi"
78 | $$$ThrowOnError(settings.%Save())
79 | do $$$AssertEquals(##class(SourceControl.Git.Utils).Name("Ens.Config.DefaultSettings.esd"),"config/test/Ens.Config.DefaultSettings.esd")
80 | do $$$AssertEquals(##class(SourceControl.Git.Utils).Name("test.class.cls"),$zconvert($namespace,"l")_"/cls/test/class.cls")
81 | do $$$AssertEquals(##class(SourceControl.Git.Utils).Name("test.routine.inc"),"mdi/inc/test/routine.inc")
82 | } catch err {
83 | do $$$AssertStatusOK(err.AsStatus())
84 | }
85 | if $data(settings)#2 && $data(oldEnvName)#2 {
86 | set settings.environmentName = oldEnvName
87 | $$$ThrowOnError(settings.%Save())
88 | }
89 | }
90 |
91 | Method OnBeforeAllTests() As %Status
92 | {
93 | merge ..Mappings = @##class(SourceControl.Git.Utils).MappingsNode()
94 | kill @##class(SourceControl.Git.Utils).MappingsNode()
95 |
96 | set $$$SourceMapping("ACB","XZY")="acb/"
97 |
98 | set $$$SourceMapping("CLS", "*") = "cls/"
99 | set $$$SourceMapping("CLS", "UnitTest") = "test/"
100 | set $$$SourceMapping("DFI", "*", "NoFolders") = 1
101 | set $$$SourceMapping("DFI", "*") = "test/_resources/dfi/"
102 |
103 | set $$$SourceMapping("NF", "*", "NoFolders") = 1
104 | set $$$SourceMapping("NF", "*") = "nf/sf/"
105 |
106 | set $$$SourceMapping("CLS", "Hello", "NoFolders") = 1
107 | set $$$SourceMapping("CLS", "Hello") = "hello/"
108 |
109 | set $$$SourceMapping("MAC","*")="rtn/"
110 | set $$$SourceMapping("MAC","*","NoFolders")=1
111 | set $$$SourceMapping("MAC","TestPackage")="rtn/"
112 |
113 | quit $$$OK
114 | }
115 |
116 | Method %OnClose() As %Status
117 | {
118 | kill @##class(SourceControl.Git.Utils).MappingsNode()
119 | merge @##class(SourceControl.Git.Utils).MappingsNode() = ..Mappings
120 | quit $$$OK
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/cls/SourceControl/Git/PullEventHandler/IncrementalLoad.cls:
--------------------------------------------------------------------------------
1 | Include (%occStatus, %occErrors, SourceControl.Git)
2 |
3 | Class SourceControl.Git.PullEventHandler.IncrementalLoad Extends SourceControl.Git.PullEventHandler
4 | {
5 |
6 | Parameter NAME = "Incremental Load";
7 |
8 | Parameter DESCRIPTION = "Performs an incremental load and compile of all changes pulled.";
9 |
10 | Method OnPull() As %Status
11 | {
12 | set sc = $$$OK
13 |
14 | // certain items must be imported before everything else.
15 | for i=1:1:$get(..ModifiedFiles) {
16 | set internalName = ..ModifiedFiles(i).internalName
17 | if internalName = ##class(SourceControl.Git.Settings.Document).#INTERNALNAME {
18 | set sc = $$$ADDSC(sc, ##class(SourceControl.Git.Utils).ImportItem(internalName, 1))
19 | quit
20 | }
21 | }
22 |
23 | set nFiles = 0
24 |
25 | for i=1:1:$get(..ModifiedFiles){
26 | set internalName = ..ModifiedFiles(i).internalName
27 |
28 | // Don't import the config file a second time
29 | continue:internalName=##class(SourceControl.Git.Settings.Document).#INTERNALNAME
30 |
31 | if ((internalName = "") && (..ModifiedFiles(i).changeType '= "D")) {
32 | write !, ..ModifiedFiles(i).externalName, " was not imported into the database and will not be compiled. "
33 | } elseif (..ModifiedFiles(i).changeType = "D") {
34 | set delSC = ..DeleteFile(internalName, ..ModifiedFiles(i).externalName)
35 | if delSC {
36 | write !, ..ModifiedFiles(i).externalName, " was deleted."
37 | } else {
38 | write !, "WARNING: Deletion of ", ..ModifiedFiles(i).externalName, " failed."
39 | }
40 | } else {
41 | set nFiles = nFiles + 1
42 | if (##class(SourceControl.Git.Utils).Type(internalName) = "ptd") {
43 | set ptdList(internalName) = ""
44 | } else {
45 | set compilelist(internalName) = ""
46 | set sc = $$$ADDSC(sc,##class(SourceControl.Git.Utils).ImportItem(internalName, 1))
47 | }
48 | }
49 | }
50 |
51 | if (nFiles = 0) {
52 | write !, "Nothing to compile."
53 | quit $$$OK
54 | }
55 | set sc = $$$ADDSC(sc,$system.OBJ.CompileList(.compilelist, "ckbryu"))
56 | // after compilation, deploy any PTD items
57 | set key = $order(ptdList(""))
58 | while (key '= "") {
59 | set sc = $$$ADDSC(sc, ##class(SourceControl.Git.Utils).ImportItem(key,1))
60 | set key = $order(ptdList(key))
61 | }
62 | if $$$comClassDefined("Ens.Director") && ##class(Ens.Director).IsProductionRunning() {
63 | write !,"Updating production... "
64 | set sc = $$$ADDSC(sc,##class(Ens.Director).UpdateProduction())
65 | write "done."
66 | }
67 | quit sc
68 | }
69 |
70 | Method DeleteFile(item As %String = "", externalName As %String = "") As %Status
71 | {
72 | try {
73 | set sc = $$$OK
74 | set type = $select(
75 | ##class(SourceControl.Git.Util.Production).ItemIsPTD(externalName): "ptd",
76 | 1: ##class(SourceControl.Git.Utils).Type(item)
77 | )
78 | set name = ##class(SourceControl.Git.Utils).NameWithoutExtension(item)
79 | set settings = ##class(SourceControl.Git.Settings).%New()
80 | set deleted = 1
81 | if type = "prj" {
82 | set sc = $system.OBJ.DeleteProject(name)
83 | }elseif type = "cls" {
84 | if ##class(SourceControl.Git.Utils).ItemIsProductionToDecompose(name) {
85 | write !, "Production decomposition enabled, skipping delete of production class"
86 | } else {
87 | set sc = $system.OBJ.Delete(item)
88 | }
89 | }elseif $listfind($listbuild("mac","int","inc","bas","mvb","mvi"), type) > 0 {
90 | set sc = ##class(%Routine).Delete(item)
91 | }elseif type = "csp" {
92 | set sc = $System.CSP.DeletePage(item)
93 | } elseif settings.decomposeProductions && (type = "ptd") {
94 | set normalizedFilePath = ##class(%File).NormalizeFilename(##class(SourceControl.Git.Utils).TempFolder()_externalName)
95 | set sc = ##class(%SYSTEM.Status).AppendStatus(
96 | ##class(SourceControl.Git.Production).RemoveItemByExternalName(normalizedFilePath,"FullExternalName"),
97 | ##class(%Library.RoutineMgr).Delete(item)
98 | )
99 | }elseif ##class(SourceControl.Git.Utils).UserTypeCached(item) {
100 | set sc = ##class(%Library.RoutineMgr).Delete(item)
101 | } else {
102 | set deleted = 0
103 | }
104 |
105 | if deleted && $$$ISOK(sc) {
106 | if (item '= "") {
107 | do ##class(SourceControl.Git.Utils).RemoveRoutineTSH(item)
108 | kill $$$TrackedItems(##class(%Studio.SourceControl.Interface).normalizeName(item))
109 | }
110 | } else {
111 | if +$system.Status.GetErrorCodes(sc) = $$$ClassDoesNotExist {
112 | // if something we wanted to delete is already deleted -- good!
113 | set sc = $$$OK
114 | }
115 | }
116 | // Force the catch if failing
117 | $$$ThrowOnError(sc)
118 | } catch e {
119 | set filename = ##class(SourceControl.Git.Utils).FullExternalName(item)
120 | if (filename = "") || '##class(%File).Exists(filename) {
121 | do ##class(SourceControl.Git.Utils).RemoveRoutineTSH(item)
122 | // file doesn't exist anymore despite error -- should be ok
123 | set sc = $$$OK
124 | } else {
125 | // Item still exists and was not deleted -- bad
126 | set sc = e.AsStatus()
127 | do e.Log()
128 | }
129 | }
130 | return sc
131 | }
132 |
133 | }
134 |
--------------------------------------------------------------------------------
/cls/_zpkg/isc/sc/git/Socket.cls:
--------------------------------------------------------------------------------
1 | Class %zpkg.isc.sc.git.Socket Extends %CSP.WebSocket
2 | {
3 |
4 | Parameter CSPURL = "/isc/studio/usertemplates/gitsourcecontrol/%zpkg.isc.sc.git.Socket.cls";
5 |
6 | Property OriginallyRedirected;
7 |
8 | Property OriginalMnemonic;
9 |
10 | Property OriginalDevice;
11 |
12 | ClassMethod Run()
13 | {
14 | If %request.Get("method") = "preview" {
15 | Set branchName = ##class(SourceControl.Git.Utils).GetCurrentBranch()
16 | Write !,"Current namespace: ",$NAMESPACE
17 | Write !,"Current branch: ",branchName
18 | Do ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "fetch")
19 | Kill errStream, outStream
20 | Do ##class(SourceControl.Git.Utils).RunGitWithArgs(.errStream, .outStream, "log", "HEAD..origin/"_branchName, "--name-status")
21 | Do ##class(SourceControl.Git.Utils).PrintStreams(errStream, outStream)
22 | If (outStream.Size = 0) && (errStream.Size = 0) {
23 | Write !,"Already up to date."
24 | }
25 | } ElseIf %request.Get("method") = "pull" {
26 | Do ##class(SourceControl.Git.API).Pull()
27 | } ElseIf %request.Get("method") = "init" {
28 | set root = %request.Get("root")
29 |
30 | // Use user input if provided
31 | if (root '= "") {
32 | set settings = ##class(SourceControl.Git.Settings).%New()
33 | set settings.namespaceTemp = root
34 | $$$ThrowOnError(settings.%Save())
35 | if ($extract(root, $length(root)) = "\") || ($extract(root, $length(root)) = "/") {
36 | set root = $extract(root, 1, $length(root) - 1)
37 | }
38 | set root = $translate(root, "\", "/")
39 | do ##class(SourceControl.Git.Utils).RunGitCommandWithInput("config",,,,"--global", "--add", "safe.directory", root)
40 | }
41 |
42 | Do ##class(SourceControl.Git.Utils).Init()
43 | Write !,"Done."
44 | } ElseIf %request.Get("method") = "clone" {
45 | Set remote = %request.Get("remote")
46 | Do ##class(SourceControl.Git.Utils).Clone(remote)
47 | Write !,"Done."
48 | } ElseIf %request.Get("method") = "sshkeygen" {
49 | Do ##class(SourceControl.Git.Utils).GenerateSSHKeyPair()
50 | Write !,"Done."
51 | } Else {
52 | Write !!,"Invalid method selected.",!!
53 | }
54 | }
55 |
56 | Method OnPreServer() As %Status
57 | {
58 | If '$SYSTEM.Security.Check("%Development","USE") {
59 | Quit $$$ERROR($$$AccessDenied)
60 | }
61 | If (%request.Get("$NAMESPACE") '= "") {
62 | Set $NAMESPACE = %request.Get("$NAMESPACE")
63 | }
64 | Quit $$$OK
65 | }
66 |
67 | Method Server() As %Status
68 | {
69 | New %server
70 | Set tSC = $$$OK
71 | Set tRedirected = 0
72 | Try {
73 | $$$ThrowOnError(..StartOutputCapture())
74 | Set tRedirected = 1
75 |
76 | // In subclasses: Do Something that produces output to the current device.
77 | // It will be sent back to the client, Base64-encoded, over the web socket connection.
78 | Do ..Run()
79 | } Catch e {
80 | Do e.Log()
81 | Write !,"An error occurred. More details can be found in the Application error log."
82 | Write !,$SYSTEM.Status.GetErrorText(e.AsStatus())
83 | Set tSC = e.AsStatus()
84 | }
85 |
86 | // Cleanup
87 | If tRedirected {
88 | Do ..EndOutputCapture()
89 | }
90 | Do ..EndServer()
91 | Quit tSC
92 | }
93 |
94 | Method StartOutputCapture() [ ProcedureBlock = 0 ]
95 | {
96 | New tSC, tRedirected
97 | #dim ex As %Exception.AbstractException
98 | #dim tSC As %Status = $$$OK
99 | #dim tRedirected As %Boolean = 0
100 | Try {
101 | Set %server = $THIS
102 | Set ..OriginallyRedirected = 0
103 | Set ..OriginalMnemonic = ""
104 | Set ..OriginalDevice = $IO
105 |
106 | Set ..OriginallyRedirected = ##class(%Library.Device).ReDirectIO()
107 | Set ..OriginalMnemonic = ##class(%Library.Device).GetMnemonicRoutine()
108 | Use ..OriginalDevice::("^"_$ZNAME)
109 | Set tRedirected = 1
110 | Do ##class(%Library.Device).ReDirectIO(1)
111 | } Catch ex {
112 | Set tSC = ex.AsStatus()
113 |
114 | // In case of exception, clean up.
115 | If tRedirected && ##class(%Library.Device).ReDirectIO(0) {
116 | Use ..OriginalDevice
117 | }
118 | If (..OriginalMnemonic '= "") {
119 | Use ..OriginalDevice::("^"_..OriginalMnemonic)
120 | }
121 | If ..OriginallyRedirected {
122 | Do ##class(%Library.Device).ReDirectIO(1)
123 | }
124 | }
125 |
126 | Quit tSC
127 |
128 | #; Public entry points for I/O redirection
129 | wstr(s) Do write(s)
130 | Quit
131 | wchr(a) Do write($CHAR(a))
132 | Quit
133 | wnl Do write($$$EOL)
134 | Set $X = 0
135 | Quit
136 | wff Do wnl Quit
137 | wtab(n) New tTab
138 | Set tTab = $JUSTIFY("",$SELECT(n>$X:n-$X,1:0))
139 | Do write(tTab)
140 | Quit
141 | write(str)
142 | // If there was an argumentless NEW, cache the output and leave it at that.
143 | // This will be output next time there's a write with %server in scope.
144 | If '$ISOBJECT($GET(%server)) {
145 | Set ^||OutputCapture.Cache($INCREMENT(^||OutputCapture.Cache)) = str
146 | Quit
147 | }
148 |
149 | // Restore previous I/O redirection settings.
150 | New tOriginalDevice,i
151 | Set tOriginalDevice = $IO
152 | If ##class(%Library.Device).ReDirectIO(0) {
153 | Use tOriginalDevice
154 | }
155 | If (%server.OriginalMnemonic '= "") {
156 | Use tOriginalDevice::("^"_%server.OriginalMnemonic)
157 | }
158 | If %server.OriginallyRedirected {
159 | Do ##class(%Library.Device).ReDirectIO(1)
160 | }
161 |
162 | If $DATA(^||OutputCapture.Cache) {
163 | For i=1:1:$GET(^||OutputCapture.Cache) {
164 | Do reallywrite(^||OutputCapture.Cache(i))
165 | }
166 | Kill ^||OutputCapture.Cache
167 | }
168 |
169 | // Write out Base64-Encoded string
170 | Do reallywrite(str)
171 |
172 | // Turn I/O redirection back on.
173 | Do ##class(%Library.Device).ReDirectIO(1)
174 | Use tOriginalDevice::("^"_$ZNAME)
175 | Quit
176 | reallywrite(pString)
177 | New tMsg
178 | Set tMsg = {"content":(pString)} // This is handy because it handles escaping of newlines, etc.
179 | Do %server.Write($SYSTEM.Encryption.Base64Encode(tMsg.%ToJSON()))
180 | Quit
181 | rstr(len, time) Quit ""
182 | rchr(time) Quit ""
183 | }
184 |
185 | Method EndOutputCapture()
186 | {
187 | Set tSC = $$$OK
188 | Try {
189 | If (..OriginalMnemonic '= "") {
190 | Use ..OriginalDevice::("^"_..OriginalMnemonic)
191 | }
192 | If ..OriginallyRedirected {
193 | Do ##class(%Library.Device).ReDirectIO(1)
194 | }
195 | } Catch e {
196 | Set tSC = e.AsStatus()
197 | }
198 | Quit tSC
199 | }
200 |
201 | Method SendJSON(pObject As %DynamicAbstractObject)
202 | {
203 | Set tOriginalDevice = $IO
204 | If ##class(%Library.Device).ReDirectIO(0) {
205 | Use tOriginalDevice
206 | }
207 | If (..OriginalMnemonic '= "") {
208 | Use tOriginalDevice::("^"_..OriginalMnemonic)
209 | }
210 | If ..OriginallyRedirected {
211 | Do ##class(%Library.Device).ReDirectIO(1)
212 | }
213 | Do ..Write($SYSTEM.Encryption.Base64Encode(pObject.%ToJSON()))
214 | Do ##class(%Library.Device).ReDirectIO(1)
215 | Use tOriginalDevice::("^"_$ZNAME)
216 | }
217 |
218 | }
219 |
--------------------------------------------------------------------------------
/test/UnitTest/SourceControl/Git/Settings.cls:
--------------------------------------------------------------------------------
1 | Class UnitTest.SourceControl.Git.Settings Extends %UnitTest.TestCase
2 | {
3 |
4 | Property SourceControlGlobal [ MultiDimensional ];
5 |
6 | Property InitialExtension As %String [ InitialExpression = {##class(%Studio.SourceControl.Interface).SourceControlClassGet()} ];
7 |
8 | Method SampleSettingsJSON()
9 | {
10 | return {
11 | "pullEventClass": "pull event class",
12 | "percentClassReplace": "x",
13 | "decomposeProductions": true,
14 | "generatedFilesReadOnly": true,
15 | "Mappings": {
16 | "TUV": {
17 | "*": {
18 | "directory": "tuv/"
19 | },
20 | "UnitTest": {
21 | "directory": "tuv2/",
22 | "noFolders": true
23 | }
24 | },
25 | "XYZ": {
26 | "*": {
27 | "directory": "xyz/"
28 | }
29 | }
30 | }
31 | }
32 | }
33 |
34 | Method TestJSONImportExport()
35 | {
36 | set settingsDynObj = ..SampleSettingsJSON()
37 | set settings = ##class(SourceControl.Git.Settings).%New()
38 | set settings.decomposeProductions = ""
39 | set settings.percentClassReplace = ""
40 | set settings.pullEventClass = ""
41 | set settings.generatedFilesReadOnly = 0
42 | do settings.ImportDynamicObject(settingsDynObj)
43 | do $$$AssertEquals(settings.decomposeProductions, 1)
44 | do $$$AssertEquals(settings.percentClassReplace, "x")
45 | do $$$AssertEquals(settings.pullEventClass, "pull event class")
46 | do $$$AssertEquals(settings.generatedFilesReadOnly, 1)
47 | do $$$AssertEquals($get(settings.Mappings("TUV","*")),"tuv/")
48 | do $$$AssertEquals($get(settings.Mappings("TUV","UnitTest")),"tuv2/")
49 | do $$$AssertTrue($get(settings.Mappings("TUV","UnitTest","NoFolders")))
50 | do $$$AssertEquals($get(settings.Mappings("XYZ","*")),"xyz/")
51 |
52 | $$$ThrowOnError(settings.%Save())
53 | set document = ##class(%RoutineMgr).%OpenId(##class(SourceControl.Git.Settings.Document).#INTERNALNAME)
54 | set settingsDynObj = ##class(%DynamicObject).%FromJSON(document.Code)
55 | do $$$AssertEquals(settingsDynObj.decomposeProductions, 1)
56 | do $$$AssertEquals(settingsDynObj.percentClassReplace, "x")
57 | do $$$AssertEquals(settingsDynObj.pullEventClass, "pull event class")
58 | do $$$AssertEquals(settingsDynObj.generatedFilesReadOnly, 1)
59 | do $$$AssertEquals(settingsDynObj.Mappings."TUV"."*".directory,"tuv/")
60 | do $$$AssertEquals(settingsDynObj.Mappings."TUV"."UnitTest".directory,"tuv2/")
61 | do $$$AssertTrue(settingsDynObj.Mappings."TUV"."UnitTest".noFolders)
62 | do $$$AssertEquals(settingsDynObj.Mappings."XYZ"."*".directory,"xyz/")
63 | }
64 |
65 | Method TestSaveAndImportSettings()
66 | {
67 | // save settings
68 | set settings = ##class(SourceControl.Git.Settings).%New()
69 | set settings.Mappings("CLS","Foo") = "foo/"
70 | set settings.pullEventClass = "SourceControl.Git.PullEventHandler.Default"
71 | set settings.percentClassReplace = "_"
72 | set settings.decomposeProductions = 1
73 | set settings.generatedFilesReadOnly = 1
74 | $$$ThrowOnError(settings.SaveWithSourceControl())
75 | do $$$AssertStatusOK(##class(SourceControl.Git.Utils).AddToSourceControl("embedded-git-config.GSC"))
76 | // settings file should be in source control
77 | do $$$AssertTrue(##class(SourceControl.Git.Utils).IsInSourceControl("embedded-git-config.GSC"))
78 | do $$$AssertEquals($replace(##class(SourceControl.Git.Utils).ExternalName("embedded-git-config.GSC"),"\","/"),"embedded-git-config.json")
79 | // commit settings
80 | do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Commit("embedded-git-config.GSC"))
81 | // settings should be in the global
82 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo/")
83 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.Default")
84 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"_")
85 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"1")
86 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","generatedFilesReadOnly"),"1")
87 | // change and save settings
88 | set settings.Mappings("CLS","Foo") = "foo2/"
89 | set settings.pullEventClass = "SourceControl.Git.PullEventHandler.IncrementalLoad"
90 | set settings.percentClassReplace = "x"
91 | set settings.decomposeProductions = 0
92 | set settings.generatedFilesReadOnly = 0
93 | $$$ThrowOnError(settings.SaveWithSourceControl())
94 | // new setting should be in the global
95 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo2/")
96 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.IncrementalLoad")
97 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"x")
98 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"0")
99 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","generatedFilesReadOnly"),"0")
100 | // revert change to settings
101 | do $$$AssertStatusOK(##class(SourceControl.Git.Utils).Revert("embedded-git-config.GSC"))
102 | // old setting should be in the global
103 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","mappings","CLS","Foo"),"foo/")
104 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","pullEventClass"),"SourceControl.Git.PullEventHandler.Default")
105 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","percentClassReplace"),"_")
106 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","decomposeProductions"),"1")
107 | do $$$AssertEquals(^SYS("SourceControl","Git","settings","generatedFilesReadOnly"),"1")
108 | }
109 |
110 | Method OnBeforeAllTests() As %Status
111 | {
112 | merge ..SourceControlGlobal = ^SYS("SourceControl")
113 | return $$$OK
114 | }
115 |
116 | Method OnBeforeOneTest() As %Status
117 | {
118 | kill ^SYS("SourceControl")
119 | do ##class(%Studio.SourceControl.Interface).SourceControlClassSet("SourceControl.Git.Extension")
120 | set settings = ##class(SourceControl.Git.Settings).%New()
121 | set settings.namespaceTemp = ##class(%Library.File).TempFilename()_"dir"
122 | $$$ThrowOnError(settings.%Save())
123 | set workMgr = $System.WorkMgr.%New("")
124 | $$$ThrowOnError(workMgr.Queue("##class(SourceControl.Git.Utils).Init"))
125 | $$$ThrowOnError(workMgr.WaitForComplete())
126 | quit $$$OK
127 | }
128 |
129 | Method %OnClose() As %Status
130 | {
131 | do ##class(%Studio.SourceControl.Interface).SourceControlClassSet(..InitialExtension)
132 | kill ^SYS("SourceControl")
133 | merge ^SYS("SourceControl") = ..SourceControlGlobal
134 | quit $$$OK
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/git-webui/src/share/git-webui/webui/img/branch.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
166 |
--------------------------------------------------------------------------------