Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions cls/SourceControl/Git/Utils.cls
Original file line number Diff line number Diff line change
Expand Up @@ -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._/-]+")
}

/// <p>This function performs <code>git checkout -b [newBranchName]</code> from the current commit.</p>
/// <p>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 <code>git checkout [defaultMergeBranch] && git pull</code>.</p>
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)
Expand Down
34 changes: 31 additions & 3 deletions git-webui/release/share/git-webui/webui/js/git-webui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '\\"') + '"';
}
Expand Down Expand Up @@ -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] == ')') {
Expand All @@ -469,8 +473,12 @@ webui.SideBarView = function(mainView, noEventHandlers) {
var cardDiv = $('<div class="card custom-card branch-card">').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 = {
Expand Down Expand Up @@ -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 = $('<div class="card-header" id="heading-' + itemId +'">').appendTo(cardDiv);
Expand All @@ -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.";
$('<div class="alert alert-warning mt-2 p-2" style="font-size: 0.8em;" role="alert">' +
warningMsg +
'</div>').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;
Expand Down Expand Up @@ -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) {
Expand Down
34 changes: 31 additions & 3 deletions git-webui/src/share/git-webui/webui/js/git-webui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '\\"') + '"';
}
Expand Down Expand Up @@ -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] == ')') {
Expand All @@ -469,8 +473,12 @@ webui.SideBarView = function(mainView, noEventHandlers) {
var cardDiv = $('<div class="card custom-card branch-card">').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 = {
Expand Down Expand Up @@ -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 = $('<div class="card-header" id="heading-' + itemId +'">').appendTo(cardDiv);
Expand All @@ -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.";
$('<div class="alert alert-warning mt-2 p-2" style="font-size: 0.8em;" role="alert">' +
warningMsg +
'</div>').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;
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 11 additions & 0 deletions test/UnitTest/SourceControl/Git/Utils/NewBranch.cls
Original file line number Diff line number Diff line change
@@ -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"))
}

}
Loading