diff --git a/gitcmds/gitcmds.go b/gitcmds/gitcmds.go index a62b54f..f02e785 100644 --- a/gitcmds/gitcmds.go +++ b/gitcmds/gitcmds.go @@ -1836,12 +1836,69 @@ func GetIssueNameByNumber(issueNum string, parentrepo string) (string, error) { return issue.Title, nil } +// normalizeBranchName normalizes a branch name to comply with Git branch naming rules. +// It replaces all invalid characters with dashes and ensures the name follows Git conventions. +// Rules applied: +// - Replace invalid characters (ASCII control chars, ~, ^, :, ?, [, *, spaces, ..) with dash +// - Remove leading/trailing dots, slashes, and dashes +// - Replace consecutive dashes with a single dash +// - Ensure lowercase for consistency +func normalizeBranchName(branchName string) string { + if branchName == "" { + return branchName + } + + // Define characters that should be replaced with dash + invalidChars := []string{ + " ", "~", "^", ":", "?", "[", "]", "*", "\\", "#", "!", + "\t", "\n", "\r", // whitespace characters + "\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", // ASCII control chars + "\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f", + "\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17", + "\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f", + "\x7f", // DEL character + } + + // Replace invalid characters with dash + normalized := branchName + for _, char := range invalidChars { + normalized = strings.ReplaceAll(normalized, char, "-") + } + + // Replace ".." (double dot) with single dash + normalized = strings.ReplaceAll(normalized, "..", "-") + + // Remove leading dots + normalized = strings.TrimLeft(normalized, ".") + + // Remove trailing slashes + normalized = strings.TrimRight(normalized, "/") + + // Remove trailing dash + normalized = strings.TrimRight(normalized, "-") + + // Remove leading and trailing dashes and underscores + normalized = strings.Trim(normalized, "-_") + + // Replace consecutive dashes with a single dash + re := regexp.MustCompile(`-+`) + normalized = re.ReplaceAllString(normalized, "-") + + return normalized +} + // CreateDevBranch creates dev branch and pushes it to origin // Parameters: // branch - branch name // notes - notes for branch // checkRemoteBranchExistence - if true, checks if a branch already exists in remote func CreateDevBranch(wd, branchName string, notes []string, checkRemoteBranchExistence bool) error { + // Normalize branch name to comply with Git naming rules + branchName = normalizeBranchName(branchName) + if branchName == "" { + return errors.New("branch name is empty after normalization") + } + mainBranch, err := GetMainBranch(wd) if err != nil { return fmt.Errorf(errMsgFailedToGetMainBranch, err) diff --git a/gitcmds/gitcmds_test.go b/gitcmds/gitcmds_test.go index 552060d..dc30949 100644 --- a/gitcmds/gitcmds_test.go +++ b/gitcmds/gitcmds_test.go @@ -50,3 +50,99 @@ func TestGetBody(t *testing.T) { body = GetBodyFromNotes(notes) require.Equal(t, "Resolves #324", body) } + +func TestNormalizeBranchName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + { + name: "simple branch name", + input: "feature-123", + expected: "feature-123", + }, + { + name: "branch with spaces", + input: "feature 123 test", + expected: "feature-123-test", + }, + { + name: "branch with invalid characters", + input: "feature~123^test:branch?name", + expected: "feature-123-test-branch-name", + }, + { + name: "branch with double dots", + input: "feature..123", + expected: "feature-123", + }, + { + name: "branch with leading dot", + input: ".feature-123", + expected: "feature-123", + }, + { + name: "branch with trailing slash", + input: "feature-123/", + expected: "feature-123", + }, + { + name: "branch with leading and trailing dashes", + input: "-feature-123-", + expected: "feature-123", + }, + { + name: "branch with consecutive dashes", + input: "feature---123-", + expected: "feature-123", + }, + { + name: "branch with uppercase", + input: "Feature-123-Test", + expected: "Feature-123-Test", + }, + { + name: "branch with special characters", + input: "feature[123]*test", + expected: "feature-123-test", + }, + { + name: "branch with backslash", + input: "feature\\123", + expected: "feature-123", + }, + { + name: "branch with tabs and newlines", + input: "feature\t123\ntest", + expected: "feature-123-test", + }, + { + name: "complex branch name", + input: "Fix: Issue #123 - Add new feature!", + expected: "Fix-Issue-123-Add-new-feature", + }, + { + name: "branch with underscores at edges", + input: "_feature-123_", + expected: "feature-123", + }, + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "only invalid characters", + input: "~~~^^^:::???", + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := normalizeBranchName(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +}