├── 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 | 2 | 3 | -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/inboxes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/gear-fill.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | image/svg+xml -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/tag.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /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 | ![Productions in Git](images/production-decomposition.png) 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 | ![Development branch in Git UI Sidebar](images/hcc/developmentsidebar.png) 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 | ![Creating a new via sidebar](images/hcc/newbranch.png) 12 | ![Naming new branch](images/hcc/newbranchnaming.png) 13 | 14 | Finally, you should click on the new branch you just made in the sidebar, and then press "Push". 15 | 16 | ![Push your new branch](images/hcc/pushbranch.png) 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 | ![Workspace tab in the sidebar](images/hcc/sidebar.png) 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. ![Changed files in workspace](images/hcc/workspacechanges.png) 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 | ![Enter commit message](images/hcc/commitmessage.png) 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 | ![Merge Branch](images/hcc/mergebranch.png) 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 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 60 | 69 | 70 | Gnome Symbolic Icon Theme 72 | 74 | 80 | 85 | 90 | 95 | 100 | 104 | 110 | 111 | 112 | 117 | 123 | 124 | -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 60 | 69 | 70 | Gnome Symbolic Icon Theme 72 | 74 | 80 | 85 | 90 | 95 | 100 | 104 | 110 | 111 | 112 | 117 | 123 | 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 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | Gnome Symbolic Icon Theme 30 | 31 | 32 | 33 | 67 | 78 | 79 | Gnome Symbolic Icon Theme 81 | 83 | 89 | 95 | 100 | 106 | 107 | 112 | 118 | 123 | 129 | 135 | 141 | 147 | 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 | 21 | 23 | 24 | 26 | image/svg+xml 27 | 29 | Gnome Symbolic Icon Theme 30 | 31 | 32 | 33 | 67 | 78 | 79 | Gnome Symbolic Icon Theme 81 | 83 | 89 | 95 | 100 | 106 | 107 | 112 | 118 | 123 | 129 | 135 | 141 | 147 | 148 | -------------------------------------------------------------------------------- /git-webui/src/share/git-webui/webui/img/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 63 | 72 | 73 | Gnome Symbolic Icon Theme 75 | 77 | 83 | 88 | 93 | 98 | 103 | 104 | 109 | 115 | 121 | 127 | 133 | 134 | -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 63 | 72 | 73 | Gnome Symbolic Icon Theme 75 | 77 | 83 | 88 | 93 | 98 | 103 | 104 | 109 | 115 | 121 | 127 | 133 | 134 | -------------------------------------------------------------------------------- /git-webui/src/share/git-webui/webui/img/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 60 | 69 | 70 | Gnome Symbolic Icon Theme 72 | 74 | 80 | 85 | 90 | 95 | 100 | 104 | 122 | 123 | 128 | 134 | 140 | 141 | -------------------------------------------------------------------------------- /git-webui/release/share/git-webui/webui/img/star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 60 | 69 | 70 | Gnome Symbolic Icon Theme 72 | 74 | 80 | 85 | 90 | 95 | 100 | 104 | 122 | 123 | 128 | 134 | 140 | 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 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | Gnome Symbolic Icon Theme 27 | 28 | 29 | 30 | 63 | 74 | 75 | Gnome Symbolic Icon Theme 77 | 79 | 85 | 90 | 95 | 100 | 110 | 120 | 130 | 135 | 136 | 141 | 147 | 153 | 159 | 165 | 166 | --------------------------------------------------------------------------------