diff --git a/CHANGELOG.md b/CHANGELOG.md index dfb1833f..3ae64c38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Changes to % routines mapped to the current namespace may now be added to source control and committed (#944) - Edits to a decomposed production in VS Code fixed after changes to the VS Code ObjectScript extension (#949) - Configure prompt no longer quits out with an unhelpful error if existing boolean settings are null (#962) +- Branch names are now constrained to a reasonable set of allowed characters, fixing an issue where branch names with special characters were hidden without explanation (#914) ## [2.16.0] - 2026-03-06 diff --git a/cls/SourceControl/Git/Utils.cls b/cls/SourceControl/Git/Utils.cls index f9ba156a..c9c1bf12 100644 --- a/cls/SourceControl/Git/Utils.cls +++ b/cls/SourceControl/Git/Utils.cls @@ -457,12 +457,22 @@ ClassMethod TakeOwnership(InternalName As %String) As %Status quit sc } +/// Validates that a branch name contains only supported characters. +/// Supported: letters (a-z, A-Z), digits (0-9), hyphens (-), underscores (_), periods (.), and forward slashes (/). +ClassMethod IsValidBranchName(branchName As %String) As %Boolean +{ + quit $match(branchName, "[a-zA-Z0-9._/-]+") +} + ///

This function performs git checkout -b [newBranchName] from the current commit.

///

If the user is in "Basic Mode" AND there is a Default Merge Branch defined, /// then this method first checks out and pulls that default merge branch before creating the new branch. This is /// equivalent to git checkout [defaultMergeBranch] && git pull.

ClassMethod NewBranch(newBranchName As %String) As %Status { + if '..IsValidBranchName(newBranchName) { + quit $$$ERROR($$$GeneralError, "Invalid branch name. Branch names may only contain letters, numbers, hyphens, underscores, periods, and forward slashes.") + } set settings = ##class(SourceControl.Git.Settings).%New() if (settings.basicMode) && (settings.defaultMergeBranch '= ""){ set err = ..RunGitWithArgs(.errStream, .outStream, "checkout", settings.defaultMergeBranch) 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 63601d2c..8239f774 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 @@ -85,6 +85,9 @@ webui.pageInfo = { isFileHistory: false }; +/// Regex for supported branch names: letters, numbers, . - _ / +webui.branchNamePattern = /^[a-zA-Z0-9._\/-]+$/; + webui.quotePath = function(path) { return '"' + path.replace(/"/g, '\\"') + '"'; } @@ -455,6 +458,7 @@ webui.SideBarView = function(mainView, noEventHandlers) { }; } + var skippedCount = 0; for (var i = 0; i < refs.length && i < maxRefsCount; ++i) { var ref = refs[i] + ""; // Get a copy of it if (ref[2] == '(' && ref[ref.length - 1] == ')') { @@ -469,8 +473,12 @@ webui.SideBarView = function(mainView, noEventHandlers) { var cardDiv = $('
').appendTo(accordionDiv)[0]; if (id.indexOf("local-branches") > -1) { // parses the output of git branch --verbose --verbose - var matches = /^\*?\s*([\w-.@&_\/]+)\s+([^\s]+)\s+(\[.*\])?.*/.exec(ref); + // only match branch names with supported characters (see webui.branchNamePattern) + var reMatchGitBranchOutput = new RegExp("^\*?\s*("+ webui.branchNamePattern.source +")\s+([^\s]+)\s+(\[.*\])?.*") + var matches = reMatchGitBranchOutput.exec(ref); if (!matches) { + $(cardDiv).remove(); + skippedCount++; continue; } var branchInfo = { @@ -507,6 +515,12 @@ webui.SideBarView = function(mainView, noEventHandlers) { } } } else { + // filter remote branches with unsupported characters + if (!webui.branchNamePattern.test(ref)) { + $(cardDiv).remove(); + skippedCount++; + continue; + } var refname = ref.replaceAll('/', '-'); var itemId = refname + idPostfix; var cardHeader = $('
').appendTo(cardDiv); @@ -527,6 +541,14 @@ webui.SideBarView = function(mainView, noEventHandlers) { }); } + if (skippedCount > 0) { + var warningMsg = skippedCount + " branch" + (skippedCount > 1 ? "es" : "") + + " with unsupported characters hidden. Branch names may only contain letters, numbers, hyphens, underscores, periods, and forward slashes."; + $('').appendTo(accordionDiv); + } + if (id === "remote-branches" && idPostfix === "popup") { var remoteBranchBtns = $("#accordion-remote-branches-popup button").filter(function (i, span) { return jQuery.inArray($(span).text(), refs) != -1; @@ -562,9 +584,15 @@ webui.SideBarView = function(mainView, noEventHandlers) { } $("#btn_createList").click(function(e) - { + { var refName = $('#newBranchName').val() - + + // Validate branch name: only allow a subset of characters. + if (!webui.branchNamePattern.test(refName)) { + alert("Invalid branch name. Branch names may only contain letters, numbers, hyphens, underscores, periods, and forward slashes."); + return; + } + var flag = 0; webui.git("status -u --porcelain", function(data) { $.get("api/uncommitted", function (uncommitted) { diff --git a/git-webui/src/share/git-webui/webui/js/git-webui.js b/git-webui/src/share/git-webui/webui/js/git-webui.js index 63601d2c..8239f774 100644 --- a/git-webui/src/share/git-webui/webui/js/git-webui.js +++ b/git-webui/src/share/git-webui/webui/js/git-webui.js @@ -85,6 +85,9 @@ webui.pageInfo = { isFileHistory: false }; +/// Regex for supported branch names: letters, numbers, . - _ / +webui.branchNamePattern = /^[a-zA-Z0-9._\/-]+$/; + webui.quotePath = function(path) { return '"' + path.replace(/"/g, '\\"') + '"'; } @@ -455,6 +458,7 @@ webui.SideBarView = function(mainView, noEventHandlers) { }; } + var skippedCount = 0; for (var i = 0; i < refs.length && i < maxRefsCount; ++i) { var ref = refs[i] + ""; // Get a copy of it if (ref[2] == '(' && ref[ref.length - 1] == ')') { @@ -469,8 +473,12 @@ webui.SideBarView = function(mainView, noEventHandlers) { var cardDiv = $('
').appendTo(accordionDiv)[0]; if (id.indexOf("local-branches") > -1) { // parses the output of git branch --verbose --verbose - var matches = /^\*?\s*([\w-.@&_\/]+)\s+([^\s]+)\s+(\[.*\])?.*/.exec(ref); + // only match branch names with supported characters (see webui.branchNamePattern) + var reMatchGitBranchOutput = new RegExp("^\*?\s*("+ webui.branchNamePattern.source +")\s+([^\s]+)\s+(\[.*\])?.*") + var matches = reMatchGitBranchOutput.exec(ref); if (!matches) { + $(cardDiv).remove(); + skippedCount++; continue; } var branchInfo = { @@ -507,6 +515,12 @@ webui.SideBarView = function(mainView, noEventHandlers) { } } } else { + // filter remote branches with unsupported characters + if (!webui.branchNamePattern.test(ref)) { + $(cardDiv).remove(); + skippedCount++; + continue; + } var refname = ref.replaceAll('/', '-'); var itemId = refname + idPostfix; var cardHeader = $('
').appendTo(cardDiv); @@ -527,6 +541,14 @@ webui.SideBarView = function(mainView, noEventHandlers) { }); } + if (skippedCount > 0) { + var warningMsg = skippedCount + " branch" + (skippedCount > 1 ? "es" : "") + + " with unsupported characters hidden. Branch names may only contain letters, numbers, hyphens, underscores, periods, and forward slashes."; + $('').appendTo(accordionDiv); + } + if (id === "remote-branches" && idPostfix === "popup") { var remoteBranchBtns = $("#accordion-remote-branches-popup button").filter(function (i, span) { return jQuery.inArray($(span).text(), refs) != -1; @@ -562,9 +584,15 @@ webui.SideBarView = function(mainView, noEventHandlers) { } $("#btn_createList").click(function(e) - { + { var refName = $('#newBranchName').val() - + + // Validate branch name: only allow a subset of characters. + if (!webui.branchNamePattern.test(refName)) { + alert("Invalid branch name. Branch names may only contain letters, numbers, hyphens, underscores, periods, and forward slashes."); + return; + } + var flag = 0; webui.git("status -u --porcelain", function(data) { $.get("api/uncommitted", function (uncommitted) { diff --git a/test/UnitTest/SourceControl/Git/Utils/NewBranch.cls b/test/UnitTest/SourceControl/Git/Utils/NewBranch.cls new file mode 100644 index 00000000..6216076d --- /dev/null +++ b/test/UnitTest/SourceControl/Git/Utils/NewBranch.cls @@ -0,0 +1,11 @@ +Class UnitTest.SourceControl.Git.Utils.NewBranch Extends UnitTest.SourceControl.Git.AbstractTest +{ + +Method TestCreateSupportedBranchNames() +{ + $$$ThrowOnError(##class(SourceControl.Git.Utils).Init()) + do $$$AssertStatusNotOK(##class(SourceControl.Git.Utils).NewBranch("not a good#branch name")) + do $$$AssertStatusOK(##class(SourceControl.Git.Utils).NewBranch("a-good/branch-name.2")) +} + +}