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 = $('