Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Branch Name Length Control Configurable #93

Merged
merged 10 commits into from
Jun 6, 2024
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ coverage:
$(eval TEST_COVERAGE_PROFILE_OUTPUT_DIRNAME=$(shell dirname $(TEST_COVERAGE_PROFILE_OUTPUT)))
$(eval TEST_REPORT_OUTPUT_DIRNAME=$(shell dirname $(TEST_REPORT_OUTPUT)))
mkdir -p $(TEST_COVERAGE_PROFILE_OUTPUT_DIRNAME) $(TEST_REPORT_OUTPUT_DIRNAME)
$(GO) test ./... -coverprofile=$(TEST_COVERAGE_PROFILE_OUTPUT) -json > $(TEST_REPORT_OUTPUT)
$(GO) test ./... -coverpkg=./... -coverprofile=$(TEST_COVERAGE_PROFILE_OUTPUT) -json > $(TEST_REPORT_OUTPUT)

.PHONY: generate-mocks
generate-mocks:
Expand Down
8 changes: 7 additions & 1 deletion internal/branches/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ func (b BranchProvider) formatBranchName(repoNameWithOwner string, branchType st
branchName = fmt.Sprintf("%s-%s", branchName, issueContext)
}

return strings.TrimSuffix(branchName[:min(63-len([]rune(repoNameWithOwner)), len([]rune(branchName)))], "-")
maxBranchNameLength := b.cfg.MaxLength - len([]rune(repoNameWithOwner))
if maxBranchNameLength > 0 {
branchNameLength := len([]rune(branchName))
branchName = branchName[:min(maxBranchNameLength, branchNameLength)]
}

return strings.TrimSuffix(branchName, "-")
}

// min returns the smallest of x or y.
Expand Down
144 changes: 141 additions & 3 deletions internal/branches/branches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,110 @@ import (
"testing"

"github.com/InditexTech/gh-sherpa/internal/config"
"github.com/InditexTech/gh-sherpa/internal/domain"
"github.com/InditexTech/gh-sherpa/internal/domain/issue_types"
domainFakes "github.com/InditexTech/gh-sherpa/internal/fakes/domain"
domainMocks "github.com/InditexTech/gh-sherpa/internal/mocks/domain"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

type BranchTestSuite struct {
suite.Suite
b *BranchProvider
userInteractionProvider *domainMocks.MockUserInteractionProvider
fakeIssue *domainFakes.FakeIssue
defaultRepository *domain.Repository
}

func TestBranchTestSuite(t *testing.T) {
suite.Run(t, new(BranchTestSuite))
}

func (s *BranchTestSuite) SetupSubTest() {
s.userInteractionProvider = &domainMocks.MockUserInteractionProvider{}

s.b = &BranchProvider{
cfg: Configuration{
Branches: config.Branches{
Prefixes: map[issue_types.IssueType]string{
issue_types.Bug: "bugfix",
},
MaxLength: 0,
},
},
UserInteraction: s.userInteractionProvider,
}

s.defaultRepository = &domain.Repository{
Name: "test-name",
Owner: "test-owner",
NameWithOwner: "test-owner/test-name",
DefaultBranchRef: "main",
}

s.fakeIssue = domainFakes.NewFakeIssue("1", issue_types.Bug, domain.IssueTrackerTypeGithub)
}

func (s *BranchTestSuite) TestGetBranchName() {

s.Run("should return expected branch name", func() {
expectedBrachName := "bugfix/GH-1-fake-title"

branchName, err := s.b.GetBranchName(s.fakeIssue, *s.defaultRepository)

s.NoError(err)
s.Equal(expectedBrachName, branchName)
})

s.Run("should return cropped branch", func() {
expectedBrachName := "bugfix/GH-1-my-title-is-too-long-and-it-sho"

s.fakeIssue.SetTitle("my title is too long and it should not matter")

s.b.cfg.Branches.MaxLength = 63
branchName, err := s.b.GetBranchName(s.fakeIssue, *s.defaultRepository)

s.NoError(err)
s.Equal(expectedBrachName, branchName)
})

s.Run("should return expected branch name when interactive", func() {
expectedBrachName := "bugfix/GH-1-fake-title-from-interactive"

s.userInteractionProvider.EXPECT().SelectOrInputPrompt(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
s.userInteractionProvider.EXPECT().SelectOrInput(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(name string, validValues []string, variable *string, required bool) {
*variable = "fake title from interactive"
}).Return(nil).Once()

s.b.cfg.IsInteractive = true

branchName, err := s.b.GetBranchName(s.fakeIssue, *s.defaultRepository)

s.NoError(err)
s.Equal(expectedBrachName, branchName)
})

s.Run("should return cropped branch name when interactive", func() {
expectedBrachName := "bugfix/GH-1-this-is-a-very-long-fake-title"

s.userInteractionProvider.EXPECT().SelectOrInputPrompt(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
s.userInteractionProvider.EXPECT().SelectOrInput(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Run(func(name string, validValues []string, variable *string, required bool) {
*variable = "this is a very long fake title from interactive"
}).Return(nil).Once()

s.b.cfg.IsInteractive = true
s.b.cfg.Branches.MaxLength = 63

branchName, err := s.b.GetBranchName(s.fakeIssue, *s.defaultRepository)

s.NoError(err)
s.Equal(expectedBrachName, branchName)
})
}

func TestParseIssueContext(t *testing.T) {
tests := []struct {
name string
Expand Down Expand Up @@ -45,6 +143,7 @@ func TestFormatBranchName(t *testing.T) {
issueId string
issueContext string
branchPrefixOverride map[issue_types.IssueType]string
maxLength int
}
tests := []struct {
name string
Expand All @@ -53,7 +152,14 @@ func TestFormatBranchName(t *testing.T) {
}{
{
name: "Does format branch name",
args: args{repository: repositoryName, branchType: "feature", issueId: "GH-1", issueContext: "my-title"},
args: args{
repository: repositoryName,
branchType: "feature",
issueId: "GH-1",
issueContext: "my-title",
maxLength: 63,
},

want: "feature/GH-1-my-title",
},
{
Expand All @@ -64,12 +170,19 @@ func TestFormatBranchName(t *testing.T) {
issueId: "GH-1",
issueContext: "my-title",
branchPrefixOverride: map[issue_types.IssueType]string{issue_types.Feature: "feat"},
maxLength: 63,
},
want: "feat/GH-1-my-title",
},
{
name: "Does format long branch name",
args: args{repository: repositoryName, branchType: "feature", issueId: "GH-1", issueContext: "my-title-is-too-long-and-it-should-be-truncated"},
args: args{
repository: repositoryName,
branchType: "feature",
issueId: "GH-1",
issueContext: "my-title-is-too-long-and-it-should-be-truncated",
maxLength: 63,
},
want: "feature/GH-1-my-title-is-too-long-and-it-s",
},
{
Expand All @@ -80,6 +193,7 @@ func TestFormatBranchName(t *testing.T) {
issueId: "GH-1",
issueContext: "my-title-is-too-long-and-it-should-be-truncated",
branchPrefixOverride: map[issue_types.IssueType]string{issue_types.Feature: "feat"},
maxLength: 63,
},
want: "feat/GH-1-my-title-is-too-long-and-it-shou",
},
Expand All @@ -91,9 +205,32 @@ func TestFormatBranchName(t *testing.T) {
issueId: "GH-1",
issueContext: "refactor-issue",
branchPrefixOverride: map[issue_types.IssueType]string{issue_types.Refactoring: ""},
maxLength: 63,
},
want: "refactoring/GH-1-refactor-issue",
},
{
name: "Does not crop the title if the maxLength is 0",
args: args{
repository: repositoryName,
branchType: "feature",
issueId: "GH-1",
issueContext: "this-is-a-very-long-title-that-will-not-be-cropped",
maxLength: 0,
},
want: "feature/GH-1-this-is-a-very-long-title-that-will-not-be-cropped",
},
{
name: "Does not crop the title if the maxLength is negative",
args: args{
repository: repositoryName,
branchType: "feature",
issueId: "GH-1",
issueContext: "this-is-a-very-long-title-that-will-not-be-cropped",
maxLength: -1,
},
want: "feature/GH-1-this-is-a-very-long-title-that-will-not-be-cropped",
},
}

for _, tt := range tests {
Expand All @@ -102,7 +239,8 @@ func TestFormatBranchName(t *testing.T) {
b := BranchProvider{
cfg: Configuration{
Branches: config.Branches{
Prefixes: tt.args.branchPrefixOverride,
Prefixes: tt.args.branchPrefixOverride,
MaxLength: tt.args.maxLength,
},
},
}
Expand Down
13 changes: 9 additions & 4 deletions internal/branches/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ func (b BranchProvider) GetBranchName(issue domain.Issue, repo domain.Repository
return "", err
}

maxContextLen := calcIssueContextMaxLen(repo.NameWithOwner, branchType, formattedID)
promptIssueContext := fmt.Sprintf("additional description (optional). Truncate to %d chars", maxContextLen)
truncatePrompt := ""
maxContextLen := b.calcIssueContextMaxLen(repo.NameWithOwner, branchType, formattedID)
if maxContextLen > 0 {
truncatePrompt = fmt.Sprintf(" Truncate to %d chars", maxContextLen)
}

promptIssueContext := "additional description (optional)." + truncatePrompt
err = b.UserInteraction.SelectOrInput(promptIssueContext, []string{}, &issueSlug, false)
if err != nil {
return "", err
Expand Down Expand Up @@ -62,10 +67,10 @@ func (b BranchProvider) getBugFixBranchType() (branchType string) {
return branchType
}

func calcIssueContextMaxLen(repository string, branchType string, issueId string) (lenIssueContext int) {
func (b BranchProvider) calcIssueContextMaxLen(repository string, branchType string, issueId string) (lenIssueContext int) {
preBranchName := fmt.Sprintf("%s/%s-", branchType, issueId)

if lenIssueContext = 63 - (len([]rune(repository)) + len([]rune(preBranchName))); lenIssueContext < 0 {
if lenIssueContext = b.cfg.MaxLength - (len([]rune(repository)) + len([]rune(preBranchName))); lenIssueContext < 0 {
lenIssueContext = 0
}

Expand Down
3 changes: 2 additions & 1 deletion internal/config/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package config
import "github.com/InditexTech/gh-sherpa/internal/domain/issue_types"

type Branches struct {
Prefixes BranchesPrefixes `validate:"validIssueTypeKeys"`
Prefixes BranchesPrefixes `validate:"validIssueTypeKeys"`
MaxLength int `mapstructure:"max_length" validate:"gte=0"`
}

type BranchesPrefixes map[issue_types.IssueType]string
9 changes: 9 additions & 0 deletions internal/config/configuration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,4 +291,13 @@ func (s *ValidateConfigTestSuite) TestConfigurationValidations() {
s.Error(err)
})

s.Run("Should return error if branches max length is negative", func() {
tCfg := s.getValidConfig()
tCfg.Branches.MaxLength = -1

err := tCfg.Validate()

s.Error(err)
})

}
4 changes: 4 additions & 0 deletions internal/config/default-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ branches:
# bugfix: myfix
# Example: map `feature` type to a branch prefix `feat/xxxx`:
# feature: feat
# Bran max length configuration
hielfx marked this conversation as resolved.
Show resolved Hide resolved
# Useful when you want to crop the branch name to a specific length.
# By default it will use 63 due to compatibilities with Kubernetes.
hielfx marked this conversation as resolved.
Show resolved Hide resolved
max_length: 63
1 change: 1 addition & 0 deletions internal/config/testdata/test-configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ branches:
prefixes:
feature: "feat"
bugfix: "fix"
max_length: 63
20 changes: 16 additions & 4 deletions internal/fakes/domain/fake_issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,34 @@ import (

type FakeIssue struct {
id string
title string
body string
url string
issueType issue_types.IssueType
issueTrackerType domain.IssueTrackerType
typeLabel string
}

var _ domain.Issue = (*FakeIssue)(nil)

func (f *FakeIssue) SetTitle(title string) {
f.title = title
}

func NewFakeIssue(id string, issueType issue_types.IssueType, issueTrackerType domain.IssueTrackerType) *FakeIssue {
return &FakeIssue{
id: id,
title: "fake title",
body: "fake body",
url: "fake url",
issueType: issueType,
issueTrackerType: issueTrackerType,
typeLabel: fmt.Sprintf("kind/%s", issueType),
}
}

func (f *FakeIssue) Body() string {
return "fake body"
return f.body
}

func (f *FakeIssue) FormatID() string {
Expand All @@ -48,13 +60,13 @@ func (f *FakeIssue) Type() issue_types.IssueType {
}

func (f *FakeIssue) TypeLabel() string {
return fmt.Sprintf("kind/%s", f.issueType)
return f.typeLabel
}

func (f *FakeIssue) Title() string {
return "fake title"
return f.title
}

func (f *FakeIssue) URL() string {
return "fake url"
return f.url
}
11 changes: 10 additions & 1 deletion pkg/validator/error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ var validationErrorMessages = map[string]string{
"validIssueTypeKeys": "Keys must be a valid issue type. Check the documentation for the list of valid issue types",
"uniqueMapValues": "Values must be unique across all keys. Check the default values for possible collisions",
}
var validationErroMessagesWithParam = map[string]string{
hielfx marked this conversation as resolved.
Show resolved Hide resolved
"gte": "Must be greater than or equal to %s",
}

func getPrettyErrors(validationErrors govalidator.ValidationErrors) string {
var buffer bytes.Buffer
Expand All @@ -23,8 +26,14 @@ func getPrettyErrors(validationErrors govalidator.ValidationErrors) string {
errKey := fieldErr.Namespace()
errMsg, ok := validationErrorMessages[fieldErr.Tag()]
if !ok {
errMsg = fmt.Sprintf(fallbackErrMessage, fieldErr.Field(), fieldErr.Tag())
errMsg, ok = validationErroMessagesWithParam[fieldErr.Tag()]
if !ok {
errMsg = fmt.Sprintf(fallbackErrMessage, fieldErr.Field(), fieldErr.Tag())
} else {
errMsg = fmt.Sprintf(errMsg, fieldErr.Param())
}
}

buffer.WriteString(fmt.Sprintf("- %s: %s\n", errKey, errMsg))
}

Expand Down