diff --git a/CHANGELOG.md b/CHANGELOG.md index f297ab62..d4d1020b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Import of decomposed production items now has a brief timeout in case another deploy is in progress (#949) +- Option to view an individual file's history in the source control menu (#960) ### Fixed - Changes to % routines mapped to the current namespace may now be added to source control and committed (#944) diff --git a/cls/SourceControl/Git/Extension.cls b/cls/SourceControl/Git/Extension.cls index 07db8e39..7f8e6d53 100644 --- a/cls/SourceControl/Git/Extension.cls +++ b/cls/SourceControl/Git/Extension.cls @@ -36,6 +36,7 @@ XData Menu +
} @@ -153,6 +155,7 @@ Method LocalizeName(name As %String) As %String "Revert":$$$Text("@Revert@Discard changes to file"), "Commit":$$$Text("@Commit@Commit changes to file"), "TakeOwnership":$$$Text("@TakeOwnership@Take ownership of changes to file"), + "FileHistory":$$$Text("@FileHistory@View file history"), "Sync":$$$Text("@Sync@Sync"), "Push":$$$Text("@Push@Push to remote branch"), "PushForce":$$$Text("@PushForce@Push to remote branch (Force)"), @@ -175,7 +178,7 @@ Method OnSourceMenuItem(name As %String, ByRef Enabled As %String, ByRef Display } if ##class(SourceControl.Git.Utils).IsNamespaceInGit() { - if $listfind($listbuild("AddToSC", "RemoveFromSC", "Revert", "Commit", "ExportProduction", "TakeOwnership"), name) { + if $listfind($listbuild("AddToSC", "RemoveFromSC", "Revert", "Commit", "ExportProduction", "TakeOwnership", "FileHistory"), name) { quit ..OnSourceMenuContextItem(InternalName,name,.Enabled,.DisplayName) } @@ -270,6 +273,8 @@ Method OnSourceMenuContextItem(itemName As %String, menuItemName As %String, ByR if isCheckedOut && (userCheckedOut '= $username) { set Enabled = 1 } + } elseif menuItemName = "FileHistory" { + set Enabled = ##class(SourceControl.Git.Utils).IsInSourceControl(itemName) } elseif menuItemName = "ExportProduction" { set itemNameNoExt = $piece(itemName,".",1,*-1) set Enabled = (##class(SourceControl.Git.Production).IsProductionClass(itemNameNoExt,"FullExternalName")) diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index 975a1a35..7330ca09 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -10,11 +10,11 @@ Parameter InstallNamespace = "%SYS"; Parameter Slash = {$case($system.Version.GetOS(),"Windows":"\",:"/")}; /// Name of the file with version controlled items -Parameter GitMenuItems = ",Settings,Commit,Sync,Pull,Fetch,Push,Revert,"; +Parameter GitMenuItems = ",Settings,Commit,Sync,Pull,Fetch,Push,Revert,FileHistory,"; Parameter ImportAfterGitMenuItems = ",Commit,Sync,Pull,Fetch,Push,"; -Parameter GitContextMenuItems = ",%Diff,%Blame,"; +Parameter GitContextMenuItems = ",%Diff,%Blame,FileHistory,"; ClassMethod %SYSNamespaceStorage() As %String [ CodeMode = expression ] { @@ -282,6 +282,12 @@ ClassMethod UserAction(InternalName As %String, MenuName As %String, ByRef Targe } elseif (menuItemName = "GitWebUI") { set Action = 2 + externalBrowser set Target = urlPrefix _ "/isc/studio/usertemplates/gitsourcecontrol/webuidriver.csp/"_$namespace_"/"_$zconvert(InternalName,"O","URL")_"?"_urlPostfix + } elseif (menuItemName = "FileHistory") { + set Action = 2 + externalBrowser + set Target = urlPrefix _ "/isc/studio/usertemplates/gitsourcecontrol/webuidriver.csp/"_$namespace_"/"_$zconvert(InternalName,"O","URL")_"?view=file-history" + if urlPostfix '= "" { + set Target = Target_"&"_urlPostfix + } } elseif (menuItemName = "Export") || (menuItemName = "ExportForce") { write !, "==export start==",! set ec = ..ExportAll($case(menuItemName="ExportForce",1:$$$Force,:0)) diff --git a/cls/SourceControl/Git/WebUIDriver.cls b/cls/SourceControl/Git/WebUIDriver.cls index a7d808cc..b14fb364 100644 --- a/cls/SourceControl/Git/WebUIDriver.cls +++ b/cls/SourceControl/Git/WebUIDriver.cls @@ -16,6 +16,8 @@ ClassMethod HandleRequest(pagePath As %String, InternalName As %String = "", Out set responseJSON = ..UserInfo() } elseif $extract(pagePath,6,*) = "uncommitted" { set responseJSON = ..Uncommitted() + } elseif $extract(pagePath,6,*) = "page-info" { + set responseJSON = ..PageInfo(InternalName, $get(%request.Data("view",1))) } elseif $extract(pagePath,6,*) = "settings" { set responseJSON = ..GetSettingsURL(%request) } elseif $extract(pagePath, 6, *) = "get-package-version"{ @@ -380,6 +382,21 @@ ClassMethod UserInfo() As %SystemBase } } +ClassMethod PageInfo(InternalName As %String = "", View As %String = "") As %SystemBase +{ + set externalName = "" + if InternalName '= "" { + set externalName = ##class(SourceControl.Git.Utils).ExternalName(InternalName) + set externalName = $translate(externalName, "\", "/") + } + quit { + "internalName": (InternalName), + "externalName": (externalName), + "view": (View), + "isFileHistory": (View = "file-history") + } +} + ClassMethod Uncommitted() As %SystemBase { // Stub diff --git a/git-webui/release/share/git-webui/webui/js/git-webui.js b/git-webui/release/share/git-webui/webui/js/git-webui.js index 53cfdfe7..fe51b83a 100644 --- a/git-webui/release/share/git-webui/webui/js/git-webui.js +++ b/git-webui/release/share/git-webui/webui/js/git-webui.js @@ -78,6 +78,32 @@ $.get("api/home", function(homeURL){ webui.homeURL = url.url; }); +webui.pageInfo = { + internalName: "", + externalName: "", + view: "", + isFileHistory: false +}; + +webui.quotePath = function(path) { + return '"' + path.replace(/"/g, '\\"') + '"'; +} + +webui.isFileHistoryMode = function() { + return !!(webui.pageInfo && webui.pageInfo.isFileHistory && webui.pageInfo.externalName); +} + +webui.getHistoryPath = function() { + return webui.isFileHistoryMode() ? webui.pageInfo.externalName : ""; +} + +webui.getHistoryDisplayName = function() { + if (!webui.pageInfo) { + return ""; + } + return webui.pageInfo.internalName || webui.pageInfo.externalName || ""; +} + webui.showSuccess = function(message) { var messageBox = $("#message-box"); messageBox.empty(); @@ -698,7 +724,7 @@ webui.SideBarView = function(mainView, noEventHandlers) { } self.getCurrentContext = function() { - var args = window.location.href.split("webuidriver.csp/")[1].split("/"); + var args = window.location.href.split("webuidriver.csp/")[1].split("?")[0].split("/"); var context = args[0]; if (args[1] && (args[1].indexOf(".ZPM") != -1)) { context = args[1]; @@ -708,14 +734,15 @@ webui.SideBarView = function(mainView, noEventHandlers) { self.updateContext = function(context) { var urlParts = window.location.href.split("webuidriver.csp/"); - var args = urlParts[1].split("/"); + var args = urlParts[1].split("?")[0].split("/"); + var querySuffix = window.location.search || ""; if (context.indexOf(".ZPM") != -1) { args[1] = context; } else { args[0] = context; args[1] = ""; } - window.location = urlParts[0] + "webuidriver.csp/" + args.join("/"); + window.location = urlParts[0] + "webuidriver.csp/" + args.join("/") + querySuffix; self.currentContext = context; } @@ -1119,6 +1146,12 @@ webui.SideBarView = function(mainView, noEventHandlers) { $("#sidebar-home", self.element).remove(); } + if (webui.isFileHistoryMode()) { + $("#sidebar-workspace", self.element).remove(); + $("#sidebar-stash", self.element).remove(); + $("#sidebarDiscarded", self.element).remove(); + } + self.getPackageVersion(); self.getEnvironment() @@ -1158,7 +1191,14 @@ webui.LogView = function(historyView) { content.removeChild(content.lastElementChild); } var startAt = content.childElementCount; - webui.git("log --date-order --pretty=raw --decorate=full --max-count=" + (maxCount + 1) + " " + self.nextRef + " --", function(data) { + var command = "log --date-order --pretty=raw --decorate=full --max-count=" + (maxCount + 1) + " " + self.nextRef; + var historyPath = webui.getHistoryPath(); + if (historyPath) { + command += " --follow -- " + webui.quotePath(historyPath); + } else { + command += " --"; + } + webui.git(command, function(data) { var start = 0; var count = 0; self.nextRef = undefined; @@ -1684,7 +1724,7 @@ webui.DiffView = function(sideBySide, hunkSelectionAllowed, parent, stashedCommi if (cmd) { self.gitCmd = cmd; self.gitDiffOpts = diffOpts; - if (file != self.gitFile && self.gitFile != '"undefined"') { + if (sideBySide && file != self.gitFile && self.gitFile != '"undefined"') { left.scrollTop = 0; left.scrollLeft = 0; right.scrollTop = 0; @@ -2078,6 +2118,60 @@ webui.DiffView = function(sideBySide, hunkSelectionAllowed, parent, stashedCommi }); } + self.copyContent = function() { + function writeText(text) { + if (navigator.clipboard && navigator.clipboard.writeText) { + return navigator.clipboard.writeText(text); + } + return new Promise(function(resolve, reject) { + var textArea = document.createElement('textarea'); + textArea.value = text; + textArea.style.position = 'fixed'; + textArea.style.top = '0'; + textArea.style.left = '0'; + textArea.style.width = '1px'; + textArea.style.height = '1px'; + textArea.style.padding = '0'; + textArea.style.border = 'none'; + textArea.style.outline = 'none'; + textArea.style.boxShadow = 'none'; + textArea.style.background = 'transparent'; + document.body.appendChild(textArea); + textArea.focus(); + textArea.select(); + try { + var successful = document.execCommand('copy'); + document.body.removeChild(textArea); + if (successful) { + resolve(); + } else { + reject(new Error('Copy command failed')); + } + } catch (err) { + document.body.removeChild(textArea); + reject(err); + } + }); + } + + var historyPath = webui.getHistoryPath(); + if (webui.isFileHistoryMode() && parent.currentCommit && historyPath) { + var fileSpecifier = parent.currentCommit + ":" + historyPath; + webui.git_command(["show", fileSpecifier], function(content) { + writeText(content).then(function() { + webui.showSuccess("File content copied to clipboard"); + }).catch(function(err) { + console.error('Failed to copy: ', err); + webui.showError("Unable to copy file content to clipboard."); + }); + }, function(message) { + webui.showError(message); + }); + } else { + webui.showWarning("Unable to copy file content: no file selected or history path unavailable."); + } + } + var html = '