diff --git a/Makefile b/Makefile index 0b17f1a..917d287 100644 --- a/Makefile +++ b/Makefile @@ -123,4 +123,4 @@ githooks: @echo "git hooks copied" shell: modcache imagedev - $(run) sh \ No newline at end of file + $(run) sh diff --git a/README.md b/README.md index f47e702..a5517ea 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,15 @@ stages: semantic-release: stage: semantic-release - only: - refs: - - master - before_script: - - docker pull registry.com/dataplatform/semantic-release:latest + variables: + SEMANTIC_RELEASE_VERSION: latest + dependencies: [] + except: + - master + before_script: + - docker pull registry.com/dataplatform/semantic-release:$SEMANTIC_RELEASE_VERSION script: - - docker run registry.com/dataplatform/semantic-release:latest up -git-host ${CI_SERVER_HOST} -git-group ${CI_PROJECT_NAMESPACE} -git-project ${CI_PROJECT_NAME} -username ${PPD2_USERNAME} -password ${PPD2_ACCESS_TOKEN} - + - docker run registry.com/dataplatform/semantic-release:$SEMANTIC_RELEASE_VERSION up -git-host ${CI_SERVER_HOST} -git-group ${CI_PROJECT_NAMESPACE} -git-project ${CI_PROJECT_NAME} -username ${PPD2_USERNAME} -password ${PPD2_ACCESS_TOKEN} ``` If your project is a Python project you can add the flag `-setup-py true` to update the release version in this file too. @@ -77,6 +78,73 @@ setup( ) ``` + ### Adding pre-commit message CLI + +The `pre-commit` will validate your commit messages before the commit being accepted. +That will prevent you from having to rebase your commit history to adapt your commit messages to [semantic-release](https://github.com/NeowayLabs/semantic-release) standards. + +**Requirements** +- [Golang installation](https://go.dev/doc/install) + + +Clone pre-commit project and install it in you SO. + +``` +git clone git@github.com:NeowayLabs/pre-commit.git +``` + +``` +make install +``` + +**How to use it?** + +After adding new changes with the `git add` command, you can run `commit .` on any git project root path and follow CLI steps. + +``` +commit . +``` + + ### How to use `rebase` to rename commit message? + +If the [commit lint ci step](#how-to-add-commit-lint-stage-to-gitlab) fail, you can rebase you commit history and fix the wrong commit messages. + +With your local repository up to date with your remote branch, run the following command. +Note: `N` is the number of commits you pushed to your branch. + +``` +git rebase -i HEAD~N +``` + +Edit the document and replace the first word `pick` to `r` or `reword`. Save it with `ctrl+o` and close it with `ctrl+x`; + +Force push it with `-f` tag as follows: + +``` +git push -f origin my-branch-name +``` + + ### How to add commit lint stage to Gitlab? + + You must add a new stage to `gitlab-ci.yml` file adding two new arguments to semantic-release script. + - `-commit-lint=true` to run commit-lint logic; + - `-branch-name=${CI_COMMIT_REF_NAME}` so that semantic-release can validate only the commits of the referenced branch. + +```yaml +stages: + - commit-lint + +commit-lint: + stage: commit-int + variables: + SEMANTIC_RELEASE_VERSION: latest + dependencies: [] + before_script: + - docker pull registry.com/dataplatform/semantic-release:$SEMANTIC_RELEASE_VERSION + script: + - docker run registry.com/dataplatform/semantic-release:$SEMANTIC_RELEASE_VERSION up -commit-lint=true -branch-name=${CI_COMMIT_REF_NAME} -git-host ${CI_SERVER_HOST} -git-group ${CI_PROJECT_NAMESPACE} -git-project ${CI_PROJECT_NAME} -username ${PPD2_USERNAME} -password ${PPD2_ACCESS_TOKEN} +``` + ### If you need more information about the semantic release CLI usage you can run the following command. ``` @@ -93,21 +161,28 @@ So the semantic release can find out the commit type to define the upgrade type ``` -type: [type here]. -message: Commit message here. +type(scope?): Commit message here. +``` + +I.e. +``` +feat(fibonacci): Added new function to print the Fibonacci sequece. +``` + +The scope is optional, so you can also use the fllowing message standard. + +``` +type: Commit message here. ``` I.e. ``` -type: [feat] -message: Added new function to print the Fibonacci sequece. +feat: Added new function to print the Fibonacci sequece. ``` -### If you want to complete a Merge Request without triggering the versioning process then you can use one of the skip type tags as follows. +### If you want to complete a Merge Request without triggering the versioning process then you can use the skip type tags as follows. -- type: [skip] -- type: [skip v] -- type: [skip versioning] +- skip ## Adding new tests diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index 97601fe..8b14b2c 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -5,6 +5,8 @@ import ( "fmt" "os" + commitmessage "github.com/NeowayLabs/semantic-release/src/commit-message" + committype "github.com/NeowayLabs/semantic-release/src/commit-type" "github.com/NeowayLabs/semantic-release/src/files" "github.com/NeowayLabs/semantic-release/src/git" "github.com/NeowayLabs/semantic-release/src/log" @@ -37,6 +39,8 @@ func main() { helpCmd := flag.NewFlagSet("help", flag.ExitOnError) helpCommitCmd := flag.NewFlagSet("help-cmt", flag.ExitOnError) + commitLint := upgradeVersionCmd.Bool("commit-lint", false, "Only lint commit history if set as true. (default false)") + branchName := upgradeVersionCmd.String("branch-name", "", "Branch name to be cloned.") gitHost := upgradeVersionCmd.String("git-host", "", "Git host name. I.e.: gitlab.integration-tests.com. (required)") groupName := upgradeVersionCmd.String("git-group", "", "Git group name. (required)") projectName := upgradeVersionCmd.String("git-project", "", "Git project name. (required)") @@ -67,15 +71,28 @@ func main() { case "up": logger.Info(colorYellow + "\nSemantic Version just started the process...\n\n" + colorReset) - semantic := newSemantic(logger, upgradeVersionCmd, gitHost, groupName, projectName, username, password, upgradePyFile) - - if err := semantic.GenerateNewRelease(); err != nil { - logger.Error(err.Error()) - os.Exit(1) + semantic := newSemantic(logger, upgradeVersionCmd, gitHost, groupName, projectName, username, password, upgradePyFile, branchName) + + if *commitLint { + if *branchName == "" { + logger.Error(colorRed + "\nThe argument -branch-name must be set when --commit-lint is true.\n\n" + colorReset) + } + + logger.Info(colorYellow + "\nSemantic Version commit lint started...\n\n" + colorReset) + err := semantic.CommitLint() + if err != nil { + printCommitTypes() + printCommitMessageExample() + os.Exit(1) + } + } else { + if err := semantic.GenerateNewRelease(); err != nil { + logger.Error(err.Error()) + os.Exit(1) + } } logger.Info(colorYellow + "\nDone!" + colorReset) - case "help": printMainCommands() helpCmd.PrintDefaults() @@ -112,7 +129,6 @@ func addFilesToUpgradeList(upgradePyFile *bool, repositoryRootPath string) Upgra } func validateIncomingParams(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, groupName, projectName, username, password *string, upgradePyFile *bool) { - if *gitHost == "" { logger.Info(colorRed + "Oops! Git host name must be specified." + colorReset + "[docker run neowaylabs/semantic-release up " + colorYellow + "-git-host gitHostNameHere]" + colorReset) os.Exit(1) @@ -158,29 +174,33 @@ func printCommitTypes() { fmt.Println(colorYellow + "\n\t* [build]" + colorReset + ": Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)") fmt.Println(colorYellow + "\t* [ci]" + colorReset + ": Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)") fmt.Println(colorYellow + "\t* [docs]" + colorReset + ": Documentation only changes") + fmt.Println(colorYellow + "\t* [documentation]" + colorReset + ": ||") fmt.Println(colorYellow + "\t* [feat]" + colorReset + ": A new feature") + fmt.Println(colorYellow + "\t* [feature]" + colorReset + ": ||") fmt.Println(colorYellow + "\t* [fix]" + colorReset + ": A bug fix") fmt.Println(colorYellow + "\t* [perf]" + colorReset + ": A code change that improves performance") + fmt.Println(colorYellow + "\t* [performance]" + colorReset + ": ||") fmt.Println(colorYellow + "\t* [refactor]" + colorReset + ": A code change that neither fixes a bug nor adds a feature") fmt.Println(colorYellow + "\t* [style]" + colorReset + ": Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)") fmt.Println(colorYellow + "\t* [test]" + colorReset + ": Adding missing tests or correcting existing tests") fmt.Println(colorYellow + "\t* [skip]" + colorReset + ": Skip versioning") - fmt.Println(colorYellow + "\t* [skip versioning]" + colorReset + ": Skip versioning") - fmt.Println(colorYellow + "\t* [breaking change]" + colorReset + ": Change that will require other changes in dependant applications") - fmt.Println(colorYellow + "\t* [breaking changes]" + colorReset + ": Changes that will require other changes in dependant applications") + fmt.Println(colorYellow + "\t* [bc]" + colorReset + ": Changes that will require other changes in dependant applications") + fmt.Println(colorYellow + "\t* [breaking]" + colorReset + ": ||") + fmt.Println(colorYellow + "\t* [breaking change]" + colorReset + ": ||") } func printCommitMessageExample() { fmt.Println(colorYellow + "\nCOMMIT MESSAGE PATTERN" + colorReset) fmt.Println("\nThe commit message must follow the pattern below.") - fmt.Println("\n\ttype [commit type here], message: Commit subject here.") + fmt.Println("\n\ttype(optional scope): Commit subject message here.") fmt.Println(colorYellow + "\n\tI.e." + colorReset) - fmt.Println("\t\ttype [feat], message: Added new feature to handle postgresql database connection.") + fmt.Println("\t\tfeat(config): Added new feature to handle configs.") - fmt.Println("\n\tNote: The maximum number of characters is 150. If the commit subject exceeds it, it will be cut, keeping only the first 150 characters.") + fmt.Println("\n\tNote 1: The (scope) is optional. Semantic-release accepts the following pattern: \"type: Commit subject message here\".") + fmt.Println("\n\tNote 2: The maximum number of characters is 150. If the commit subject exceeds it, it will be cut, keeping only the first 150 characters.") } -func newSemantic(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, groupName, projectName, username, password *string, upgradePyFile *bool) *semantic.Semantic { +func newSemantic(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, groupName, projectName, username, password *string, upgradePyFile *bool, branchName *string) *semantic.Semantic { validateIncomingParams(logger, upgradeVersionCmd, gitHost, groupName, projectName, username, password, upgradePyFile) @@ -188,14 +208,17 @@ func newSemantic(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, grou repositoryRootPath := fmt.Sprintf("%s/%s", homePath, *projectName) url := fmt.Sprintf("https://%s:%s@%s/%s/%s.git", *username, *password, *gitHost, *groupName, *projectName) - repoVersionControl, err := git.New(logger, timer.PrintElapsedTime, url, *username, *password, repositoryRootPath) + repoVersionControl, err := git.New(logger, timer.PrintElapsedTime, url, *username, *password, repositoryRootPath, *branchName) if err != nil { logger.Fatal(err.Error()) } - filesVersionControl := files.New(logger, timer.PrintElapsedTime, *gitHost, repositoryRootPath, *groupName, *projectName) + commitTypeManager := committype.New(logger) + commitMessageManager := commitmessage.New(logger, commitTypeManager) + + filesVersionControl := files.New(logger, timer.PrintElapsedTime, *gitHost, repositoryRootPath, *groupName, *projectName, commitMessageManager) - versionControl := v.NewVersionControl(logger, timer.PrintElapsedTime) + versionControl := v.NewVersionControl(logger, timer.PrintElapsedTime, commitTypeManager) - return semantic.New(logger, repositoryRootPath, addFilesToUpgradeList(upgradePyFile, repositoryRootPath), repoVersionControl, filesVersionControl, versionControl) + return semantic.New(logger, repositoryRootPath, addFilesToUpgradeList(upgradePyFile, repositoryRootPath), repoVersionControl, filesVersionControl, versionControl, commitMessageManager, commitTypeManager) } diff --git a/src/commit-message/commit_message_manager.go b/src/commit-message/commit_message_manager.go new file mode 100644 index 0000000..1a6298a --- /dev/null +++ b/src/commit-message/commit_message_manager.go @@ -0,0 +1,107 @@ +package commitmessage + +import ( + "errors" + "fmt" + "strings" +) + +type Logger interface { + Info(s string, args ...interface{}) + Error(s string, args ...interface{}) + Warn(s string, args ...interface{}) +} + +type CommitType interface { + GetAll() []string + GetMajorUpgrade() []string + GetMinorUpgrade() []string + GetPatchUpgrade() []string + GetSkipVersioning() []string + GetCommitChangeType(commitMessage string) (string, error) + IndexNotFound(index int) bool +} + +type CommitMessage struct { + log Logger + commitType CommitType +} + +func (f *CommitMessage) isMessageLongerThanLimit(message string) bool { + return len(message) >= 150 +} + +func (f *CommitMessage) upperFirstLetterOfSentence(text string) string { + return fmt.Sprintf("%s%s", strings.ToUpper(text[:1]), text[1:]) +} + +// prettifyCommitMessage aims to keep a short message based on the commit message, removing extra information such as commit type. +// Args: +// +// commitMessage (string): Full commit message. +// +// Returns: +// +// string: Returns a commit message with limmited number of characters. +// err: Error whenever unexpected issues happen. +func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, error) { + splitedMessage := strings.Split(commitMessage, "\n") + + message := "" + for _, row := range splitedMessage { + index := strings.Index(row, ":") + + if f.commitType.IndexNotFound(index) || row == "" { + continue + } + + commitTypeScope := strings.ToLower(row[:index]) + + for _, changeType := range f.commitType.GetAll() { + if strings.Contains(commitTypeScope, changeType) { + message = strings.TrimSpace(strings.Replace(row[index:], ":", "", 1)) + } + } + } + + if message == "" { + return "", errors.New("commit message is empty") + } + + if f.isMessageLongerThanLimit(message) { + message = fmt.Sprintf("%s...", message[:150]) + } + + return f.upperFirstLetterOfSentence(message), nil +} + +func (f *CommitMessage) IsValidMessage(message string) bool { + index := strings.Index(message, ":") + + if f.commitType.IndexNotFound(index) { + f.log.Error("commit message out of pattern") + return false + } + + if message == "" || message[index:] == ":" { + f.log.Error("commit message cannot be empty") + return false + } + + _, err := f.commitType.GetCommitChangeType(message) + if err != nil { + if err.Error() == "change type not found" { + f.log.Error("change type not found") + } + return false + } + + return true +} + +func New(log Logger, commitType CommitType) *CommitMessage { + return &CommitMessage{ + log: log, + commitType: commitType, + } +} diff --git a/src/commit-message/commit_message_manager_test.go b/src/commit-message/commit_message_manager_test.go new file mode 100644 index 0000000..c935057 --- /dev/null +++ b/src/commit-message/commit_message_manager_test.go @@ -0,0 +1,96 @@ +//go:build unit +// +build unit + +package commitmessage_test + +import ( + "testing" + + commitMessage "github.com/NeowayLabs/semantic-release/src/commit-message" + committype "github.com/NeowayLabs/semantic-release/src/commit-type" + "github.com/NeowayLabs/semantic-release/src/log" + "github.com/NeowayLabs/semantic-release/src/tests" +) + +type fixture struct { + log *log.Log + commitMessageManager commitMessage.CommitMessage +} + +func setup(t *testing.T) *fixture { + logger, err := log.New("test", "1.0.0", "debug") + if err != nil { + t.Errorf("error while getting log due to %s", err.Error()) + } + + commitType := committype.New(logger) + commitMessageMenager := commitMessage.New(logger, commitType) + + return &fixture{log: logger, commitMessageManager: *commitMessageMenager} +} + +func TestPrettifyCommitMessageNoMessageEmptyError(t *testing.T) { + f := setup(t) + message := "feat(scope):" + prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) + tests.AssertError(t, err) + tests.AssertEmpty(t, prettyMessage) +} + +func TestPrettifyCommitMessageNewLinesSuccess(t *testing.T) { + f := setup(t) + message := "Merge branch 'sample-branch' into 'master'\n\nfeat(scope): This is a message with new lines.\n\nSee merge request gitgroup/semantic-tests!1" + prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) + tests.AssertNoError(t, err) + tests.AssertEqualValues(t, "This is a message with new lines.", prettyMessage) +} + +func TestPrettifyCommitMessageCutSuccess(t *testing.T) { + f := setup(t) + message := "feat: This is a long message to write to CHANGELOG.md file. Bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo cut here." + prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) + tests.AssertNoError(t, err) + tests.AssertEqualValues(t, "This is a long message to write to CHANGELOG.md file. Bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo ...", prettyMessage) +} + +func TestIsValidMessageSuccess(t *testing.T) { + f := setup(t) + message := "Merge branch 'sample-branch' into 'master'\n\nfeat(scope): This is a message with new lines.\n\nSee merge request gitgroup/semantic-tests!1" + actual := f.commitMessageManager.IsValidMessage(message) + tests.AssertTrue(t, actual) + + message = "feat(scope): This is a message" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertTrue(t, actual) + + message = "feat: This is a message" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertTrue(t, actual) +} + +func TestIsValidMessageFalse(t *testing.T) { + f := setup(t) + message := "Merge branch 'sample-branch' into 'master'\n\nfeat(scope) This is a message with new lines.\n\nSee merge request gitgroup/semantic-tests!1" + actual := f.commitMessageManager.IsValidMessage(message) + tests.AssertFalse(t, actual) + + message = "feat(scope):" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertFalse(t, actual) + + message = "feat:" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertFalse(t, actual) + + message = "feat This is a message with new lines" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertFalse(t, actual) + + message = "" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertFalse(t, actual) + + message = "wrong type(scope): This is a message" + actual = f.commitMessageManager.IsValidMessage(message) + tests.AssertFalse(t, actual) +} diff --git a/src/commit-type/commit_type.go b/src/commit-type/commit_type.go new file mode 100644 index 0000000..93f6e77 --- /dev/null +++ b/src/commit-type/commit_type.go @@ -0,0 +1,115 @@ +package committype + +import ( + "errors" + "regexp" + "strings" +) + +type Logger interface { + Info(s string, args ...interface{}) +} + +type CommitType struct { + log Logger +} + +func (c *CommitType) GetAll() []string { + return []string{"build", "ci", "docs", "fix", "feat", "feature", "feature", "perf", "performance", "refactor", "style", "test", "bc", "breaking", "breaking change", "skip"} +} + +func (c *CommitType) GetMajorUpgrade() []string { + return []string{"bc", "breaking", "breaking change"} +} + +func (c *CommitType) GetMinorUpgrade() []string { + return []string{"feat", "feature"} +} + +func (c *CommitType) GetPatchUpgrade() []string { + return []string{"build", "ci", "docs", "documentation", "fix", "perf", "performance", "refactor", "style", "test"} +} + +func (c *CommitType) GetSkipVersioning() []string { + return []string{"skip"} +} + +func (c *CommitType) isValidCommitType(commitTypeScope string) bool { + for _, changeType := range c.GetAll() { + if strings.Contains(commitTypeScope, changeType) { + return true + } + } + return false +} + +// GetScope get the commit scope from Message +// I.e.: +// +// fix(any): Commit subject here. +// +// Output: any +func (c *CommitType) GetScope(commitMessage string) string { + c.log.Info("getting commit scope from message %s", commitMessage) + splitedMessage := strings.Split(commitMessage, "\n") + re := regexp.MustCompile(`\((.*?)\)`) + + for _, row := range splitedMessage { + index := strings.Index(row, ":") + + if c.IndexNotFound(index) || row == " " { + continue + } + commitTypeScope := strings.ToLower(row[:index]) + + if c.isValidCommitType(commitTypeScope) { + found := re.FindAllString(row, -1) + for _, element := range found { + element = strings.Trim(element, "(") + element = strings.Trim(element, ")") + return element + } + } + } + + return "default" +} + +func (c *CommitType) IndexNotFound(index int) bool { + return index == -1 +} + +// GetCommitChangeType get the commit type from Message +// I.e.: +// +// fix(scope?): Commit subject here. +// +// Output: fix +func (c *CommitType) GetCommitChangeType(commitMessage string) (string, error) { + c.log.Info("getting commit type from message %s", commitMessage) + splitedMessage := strings.Split(commitMessage, "\n") + + for _, row := range splitedMessage { + index := strings.Index(row, ":") + + if c.IndexNotFound(index) || row == "" { + continue + } + + commitTypeScope := strings.ToLower(row[:index]) + + for _, changeType := range c.GetAll() { + if strings.Contains(commitTypeScope, changeType) { + return changeType, nil + } + } + } + + return "", errors.New("change type not found") +} + +func New(log Logger) *CommitType { + return &CommitType{ + log: log, + } +} diff --git a/src/commit-type/commit_type_test.go b/src/commit-type/commit_type_test.go new file mode 100644 index 0000000..b55c44d --- /dev/null +++ b/src/commit-type/commit_type_test.go @@ -0,0 +1,104 @@ +//go:build unit +// +build unit + +package committype_test + +import ( + "testing" + + committype "github.com/NeowayLabs/semantic-release/src/commit-type" + "github.com/NeowayLabs/semantic-release/src/log" + "github.com/NeowayLabs/semantic-release/src/tests" +) + +type fixture struct { + commitType committype.CommitType +} + +func setup(t *testing.T) *fixture { + logger, err := log.New("test", "1.0.0", "debug") + if err != nil { + t.Errorf("error while getting log due to %s", err.Error()) + } + commitType := committype.New(logger) + + return &fixture{commitType: *commitType} +} + +func TestGetAll(t *testing.T) { + f := setup(t) + expected := []string{"build", "ci", "docs", "fix", "feat", "feature", "feature", "perf", "performance", "refactor", "style", "test", "bc", "breaking", "breaking change", "skip"} + actual := f.commitType.GetAll() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetMajorUpgrade(t *testing.T) { + f := setup(t) + expected := []string{"bc", "breaking", "breaking change"} + actual := f.commitType.GetMajorUpgrade() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetMinorUpgrade(t *testing.T) { + f := setup(t) + expected := []string{"feat", "feature"} + actual := f.commitType.GetMinorUpgrade() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetPatchUpgrade(t *testing.T) { + f := setup(t) + expected := []string{"build", "ci", "docs", "documentation", "fix", "perf", "performance", "refactor", "style", "test"} + actual := f.commitType.GetPatchUpgrade() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetSkipVersioning(t *testing.T) { + f := setup(t) + expected := []string{"skip"} + actual := f.commitType.GetSkipVersioning() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetScopeDefaultSuccess(t *testing.T) { + f := setup(t) + actualScope := f.commitType.GetScope("fix: this is the message") + tests.AssertDeepEqualValues(t, "default", actualScope) +} + +func TestGetScopeSuccess(t *testing.T) { + f := setup(t) + actualScope := f.commitType.GetScope("fix(scope): this is the message") + tests.AssertDeepEqualValues(t, "scope", actualScope) +} + +func TestGetCommitChangeTypeNotFoundError(t *testing.T) { + f := setup(t) + message := "wrong type(scope): This is a sample message" + actualType, err := f.commitType.GetCommitChangeType(message) + tests.AssertError(t, err) + tests.AssertEqualValues(t, "", actualType) +} + +func TestGetCommitChangeTypeSuccess(t *testing.T) { + f := setup(t) + expected := "fix" + message := "fix(scope): This is a sample message" + actualType, err := f.commitType.GetCommitChangeType(message) + tests.AssertNoError(t, err) + tests.AssertEqualValues(t, expected, actualType) +} + +func TestGetCommitChangeTypeNewLinesSuccess(t *testing.T) { + f := setup(t) + expected := "feat" + message := "Merge branch 'sample-branch' into 'master'\n\nfeat(scope): This is a message with new lines.\n\nSee merge request gitgroup/semantic-tests!1" + actualType, err := f.commitType.GetCommitChangeType(message) + tests.AssertNoError(t, err) + tests.AssertEqualValues(t, expected, actualType) +} diff --git a/src/files/files.go b/src/files/files.go index 40a849e..f8436d0 100644 --- a/src/files/files.go +++ b/src/files/files.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os" "strings" ) @@ -13,7 +12,6 @@ import ( const ( colorYellow = "\033[33m" colorReset = "\033[0m" - messageTag = "message:" ) var ( @@ -26,6 +24,10 @@ type Logger interface { Warn(s string, args ...interface{}) } +type CommitMessageManager interface { + PrettifyCommitMessage(commitMessage string) (string, error) +} + type ElapsedTime func(functionName string) func() type ChangesInfo struct { @@ -49,13 +51,14 @@ type UpgradeFile struct { } type FileVersion struct { - log Logger - elapsedTime ElapsedTime - versionConrolHost string - repositoryRootPath string - groupName string - projectName string - variableNameFound bool + log Logger + elapsedTime ElapsedTime + versionConrolHost string + repositoryRootPath string + groupName string + projectName string + variableNameFound bool + commitMessageManager CommitMessageManager } func (f *FileVersion) openFile(filePath string) (*os.File, error) { @@ -66,67 +69,6 @@ func (f *FileVersion) openFile(filePath string) (*os.File, error) { return file, nil } -func (f *FileVersion) findMessageTag(commitMessage string) bool { - return strings.Contains(strings.ToLower(commitMessage), messageTag) -} - -func (f *FileVersion) getMessage(messageRow string) (string, error) { - startPosition := strings.Index(messageRow, messageTag) + len(messageTag) - - if startPosition-1 == len(messageRow)-1 { - return "", errors.New("message not found") - } - - message := strings.TrimSpace(messageRow[startPosition:]) - if strings.ReplaceAll(message, " ", "") == "" { - return "", errors.New("message not found") - } - - return message, nil -} - -func (f *FileVersion) isMessageLongerThanLimit(message string) bool { - return len(message) >= 150 -} - -func (f *FileVersion) upperFirstLetterOfSentence(text string) string { - return fmt.Sprintf("%s%s", strings.ToUpper(text[:1]), text[1:]) -} - -// prettifyCommitMessage aims to keep a short message based on the commit message, removing extra information such as commit type. -// Args: -// commitMessage (string): Full commit message. -// Returns: -// string: Returns a commit message with limmited number of characters. -// err: Error whenever unexpected issues happen. -func (f *FileVersion) prettifyCommitMessage(commitMessage string) (string, error) { - - var message string - splitedMessage := strings.Split(commitMessage, "\n") - - for _, row := range splitedMessage { - row := strings.ToLower(row) - if f.findMessageTag(row) { - - currentMessage, err := f.getMessage(row) - if err != nil { - return "", fmt.Errorf("error while getting message due to: %w", err) - } - message = currentMessage - } - } - - if message == "" { - return "", errors.New("commit message has no tag 'message:'") - } - - if f.isMessageLongerThanLimit(message) { - message = fmt.Sprintf("%s...", message[:150]) - } - - return f.upperFirstLetterOfSentence(message), nil -} - func (f *FileVersion) containsVariableNameInText(text, variableName, copySignal string) bool { text = strings.ReplaceAll(text, " ", "") return strings.Contains(text, fmt.Sprintf("%s%s", variableName, copySignal)) @@ -156,7 +98,7 @@ func (f *FileVersion) unmarshalUpgradeFiles(filesToUpgrade interface{}) (*Upgrad func (f *FileVersion) writeFile(destinationPath, originPath string, content []byte) error { destination := f.setDefaultPath(destinationPath, originPath) - if err := ioutil.WriteFile(destination, content, 0666); err != nil { + if err := os.WriteFile(destination, content, 0666); err != nil { return fmt.Errorf("error while writing file %s due to: %w", destination, err) } @@ -195,8 +137,9 @@ func (f *FileVersion) getFileOutputContent(scanner *bufio.Scanner, file UpgradeF // It will update the files row containing a given variable name. // I.e.: // err := UpgradeVariableInFiles(UpgradeFiles{Files: []UpgradeFile{{Path: "./setup.py", DestinationPath: "", VariableName: "__version__"}}), "1.0.1") -// From: __version__ = 1.0.0 -// To: __version__ = 1.0.1 +// +// From: __version__ = 1.0.0 +// To: __version__ = 1.0.1 func (f *FileVersion) UpgradeVariableInFiles(filesToUpgrade interface{}, newVersion string) error { defer f.elapsedTime("UpgradeVariableInFiles")() @@ -294,7 +237,7 @@ func (f *FileVersion) unmarshalChangesInfo(changes interface{}) (*ChangesInfo, e } func (f *FileVersion) formatChangeLogContent(changes *ChangesInfo) (string, error) { - commitMessage, err := f.prettifyCommitMessage(changes.Message) + commitMessage, err := f.commitMessageManager.PrettifyCommitMessage(changes.Message) if err != nil { return "", fmt.Errorf("prettify commit message error: %w", err) } @@ -359,13 +302,14 @@ func (f *FileVersion) UpgradeChangeLog(path, destinationPath string, chageLogInf return nil } -func New(log Logger, elapsedTime ElapsedTime, versionConrolHost, repositoryRootPath, groupName, projectName string) *FileVersion { +func New(log Logger, elapsedTime ElapsedTime, versionConrolHost, repositoryRootPath, groupName, projectName string, commitMessageManager CommitMessageManager) *FileVersion { return &FileVersion{ - log: log, - elapsedTime: elapsedTime, - versionConrolHost: versionConrolHost, - repositoryRootPath: repositoryRootPath, - groupName: groupName, - projectName: projectName, + log: log, + elapsedTime: elapsedTime, + versionConrolHost: versionConrolHost, + repositoryRootPath: repositoryRootPath, + groupName: groupName, + projectName: projectName, + commitMessageManager: commitMessageManager, } } diff --git a/src/files/files_test.go b/src/files/files_test.go index c8ccdf4..1ab69c1 100644 --- a/src/files/files_test.go +++ b/src/files/files_test.go @@ -7,6 +7,8 @@ import ( "fmt" "testing" + commitmessage "github.com/NeowayLabs/semantic-release/src/commit-message" + committype "github.com/NeowayLabs/semantic-release/src/commit-type" "github.com/NeowayLabs/semantic-release/src/files" "github.com/NeowayLabs/semantic-release/src/log" "github.com/NeowayLabs/semantic-release/src/tests" @@ -56,7 +58,10 @@ func setup(t *testing.T) *fixture { } func (f *fixture) newFiles() *files.FileVersion { - return files.New(f.log, printElapsedTimeMock, f.versionControlHost, f.repositoryRootPath, f.groupName, f.projectName) + commitType := committype.New(f.log) + commitMessageManager := commitmessage.New(f.log, commitType) + + return files.New(f.log, printElapsedTimeMock, f.versionControlHost, f.repositoryRootPath, f.groupName, f.projectName, commitMessageManager) } func TestUpgradeVariableInFilesNoError(t *testing.T) { @@ -133,7 +138,7 @@ func TestUpgradeChangeLogNoError(t *testing.T) { Hash: "b25a9af78c30de0d03ca2ee6d18c66bbc4804395", AuthorName: "Administrator", AuthorEmail: "admin@git.com", - Message: "type: [feat], Message: This is a short message to write to CHANGELOG.md file.", + Message: "feat(scope): This is a short message to write to CHANGELOG.md file.", CurrentVersion: "1.0.1", NewVersion: "1.1.0", ChangeType: "feat", @@ -154,7 +159,7 @@ func TestUpgradeChangeLogLongMessageCutNoError(t *testing.T) { Hash: "b25a9af78c30de0d03ca2ee6d18c66bbc4804395", AuthorName: "Administrator", AuthorEmail: "admin@git.com", - Message: "type: [feat], Message: This is a long message to write to CHANGELOG.md file. Bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo cut here.", + Message: "feat: This is a long message to write to CHANGELOG.md file. Bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo bar foo cut here.", CurrentVersion: "1.0.1", NewVersion: "1.1.0", ChangeType: "feat", @@ -297,43 +302,7 @@ func TestUpgradeChangeLogMessageNotFoundError(t *testing.T) { err := filesVersion.UpgradeChangeLog("mock/CHANGELOG_MOCK.md", "", changelog) tests.AssertError(t, err) - tests.AssertEqualValues(t, "error while formatting changelog content due to: prettify commit message error: error while getting message due to: message not found", err.Error()) -} - -func TestUpgradeChangeLogTagMessageNotFoundEmptyError(t *testing.T) { - f := setup(t) - filesVersion := f.newFiles() - - changelog := ChangesInfoMock{ - AuthorName: "Administrator", - AuthorEmail: "admin@git.com", - ChangeType: "feat", - Hash: "b25a9af", - Message: "message: ", - CurrentVersion: "1.0.0", - NewVersion: "1.1.0"} - - err := filesVersion.UpgradeChangeLog("mock/CHANGELOG_MOCK.md", "", changelog) - tests.AssertError(t, err) - tests.AssertEqualValues(t, "error while formatting changelog content due to: prettify commit message error: error while getting message due to: message not found", err.Error()) -} - -func TestUpgradeChangeLogTagMessageNotFoundError(t *testing.T) { - f := setup(t) - filesVersion := f.newFiles() - - changelog := ChangesInfoMock{ - AuthorName: "Administrator", - AuthorEmail: "admin@git.com", - ChangeType: "feat", - Hash: "b25a9af", - Message: "type: [feat]", - CurrentVersion: "1.0.0", - NewVersion: "1.1.0"} - - err := filesVersion.UpgradeChangeLog("mock/CHANGELOG_MOCK.md", "", changelog) - tests.AssertError(t, err) - tests.AssertEqualValues(t, "error while formatting changelog content due to: prettify commit message error: commit message has no tag 'message:'", err.Error()) + tests.AssertEqualValues(t, "error while formatting changelog content due to: prettify commit message error: commit message is empty", err.Error()) } func TestUpgradeChangeLogOpenFileError(t *testing.T) { @@ -345,7 +314,7 @@ func TestUpgradeChangeLogOpenFileError(t *testing.T) { AuthorEmail: "admin@git.com", ChangeType: "feat", Hash: "b25a9af", - Message: "type: [feat], message: Test.", + Message: "feat(scope): Test message.", CurrentVersion: "1.0.0", NewVersion: "1.1.0"} @@ -365,7 +334,7 @@ func TestUpgradeChangeLogWriteFileError(t *testing.T) { Hash: "b25a9af78c30de0d03ca2ee6d18c66bbc4804395", AuthorName: "Administrator", AuthorEmail: "admin@git.com", - Message: "type: [feat], Message: This is a short message to write to CHANGELOG.md file.", + Message: "feat(scope): This is a short message to write to CHANGELOG.md file.", CurrentVersion: "1.0.1", NewVersion: "1.1.0", ChangeType: "feat", diff --git a/src/git/git.go b/src/git/git.go index 8092f87..b9e3402 100644 --- a/src/git/git.go +++ b/src/git/git.go @@ -34,6 +34,7 @@ type Logger interface { type GitMethods struct { getBranchPointedToHead func() (*plumbing.Reference, error) + getBranchReference func(branchName string) (*plumbing.Reference, error) getCommitHistory func() ([]*object.Commit, error) getMostRecentCommit func() (CommitInfo, error) getAllTags func() ([]object.Tag, error) @@ -49,19 +50,22 @@ type GitMethods struct { type ElapsedTime func(functionName string) func() type GitVersioning struct { - git GitMethods - log Logger - printElapsedTime ElapsedTime - url string - destinationDirectory string - username string - password string - repo *git.Repository - branchHead *plumbing.Reference - commitHistory []*object.Commit - tagsList []object.Tag - mostRecentCommit CommitInfo - mostRecentTag string + git GitMethods + log Logger + printElapsedTime ElapsedTime + url string + destinationDirectory string + username string + password string + repo *git.Repository + branchHead *plumbing.Reference + commitHistory []*object.Commit + commitHistoryCurrentBranch []*object.Commit + commitHistoryDiff []*object.Commit + tagsList []object.Tag + mostRecentCommit CommitInfo + mostRecentTag string + branchName string } type CommitInfo struct { @@ -155,7 +159,8 @@ func (g *GitVersioning) getBranchPointedToHead() (*plumbing.Reference, error) { func (g *GitVersioning) getCommitHistory() ([]*object.Commit, error) { defer g.printElapsedTime("GetComitHistory")() - g.log.Info("getting commit history") + g.log.Info("getting commit history of branch: %s", g.branchHead.Name()) + cIter, err := g.repo.Log(&git.LogOptions{From: g.branchHead.Hash(), Order: git.LogOrderCommitterTime}) if err != nil { return nil, err @@ -173,6 +178,14 @@ func (g *GitVersioning) getCommitHistory() ([]*object.Commit, error) { return commits, nil } +func (g *GitVersioning) GetCommitHistory() []*object.Commit { + return g.commitHistory +} + +func (g *GitVersioning) GetCommitHistoryDiff() []*object.Commit { + return g.commitHistoryDiff +} + func (g *GitVersioning) isTimeAfter(timeToCheck, referenceTime time.Time) bool { return timeToCheck.After(referenceTime) } @@ -288,7 +301,7 @@ func (g *GitVersioning) commitChanges(newReleaseVersion string) error { signature := &object.Signature{Name: g.mostRecentCommit.AuthorName, Email: g.mostRecentCommit.AuthorEmail, When: time.Now()} - message := fmt.Sprintf("type: [skip]: message: Commit automatically generated by Semantic Release. The new tag is %s", newReleaseVersion) + message := fmt.Sprintf("skip: Commit automatically generated by Semantic Release. The new tag is %s", newReleaseVersion) commit, err := worktree.Commit(message, &git.CommitOptions{Author: signature, Committer: signature}) if err != nil { return err @@ -386,15 +399,17 @@ func (g *GitVersioning) pushTags() error { func (g *GitVersioning) cloneRepoToDirectory() (*git.Repository, error) { defer g.printElapsedTime("CloneRepoToDirectory")() - g.log.Info(colorYellow+"cloning repo "+colorCyan+" %s "+colorYellow+" into "+colorCyan+"%s"+colorReset, g.url, g.destinationDirectory) - repo, err := git.PlainClone(g.destinationDirectory, false, &git.CloneOptions{ + g.log.Info(colorYellow+"cloning current repository to "+colorCyan+" %s "+colorReset, g.destinationDirectory) + opts := &git.CloneOptions{ Progress: os.Stdout, URL: g.url, Auth: &http.BasicAuth{Username: g.username, Password: g.password, }, InsecureSkipTLS: true, - }) + } + + repo, err := git.PlainClone(g.destinationDirectory, false, opts) if err == nil { return repo, nil @@ -405,6 +420,7 @@ func (g *GitVersioning) cloneRepoToDirectory() (*git.Repository, error) { return git.PlainOpen(g.destinationDirectory) } g.log.Error("error while cloning gitab repository due to: %s", err) + return nil, err } @@ -417,7 +433,57 @@ func (g *GitVersioning) setBranchHead() error { return nil } +func (g *GitVersioning) getBranchReference(branchName string) (*plumbing.Reference, error) { + defer g.printElapsedTime("getBranchReference")() + branchName = fmt.Sprintf("/refs/remotes/origin/%s", branchName) + g.log.Info("getting branch pointed to %s", branchName) + ref, err := g.repo.Reference(plumbing.ReferenceName(branchName), false) + if err != nil { + return nil, err + } + + return ref, nil +} + +func (g *GitVersioning) setReferenceBranch(branchName string) error { + branchHead, err := g.git.getBranchReference(branchName) + if err != nil { + return fmt.Errorf("error while retrieving the branch pointed to %s due to: %w", branchName, err) + } + g.branchHead = branchHead + return nil +} + +func (g *GitVersioning) getCurrentBranchCommitsDiff() []*object.Commit { + var commitHistoryDiff []*object.Commit + found := false + for _, currentBranchCommit := range g.commitHistoryCurrentBranch { + for _, masterCommit := range g.commitHistory { + if currentBranchCommit.Hash == masterCommit.Hash { + found = true + break + } + } + if !found { + commitHistoryDiff = append(commitHistoryDiff, currentBranchCommit) + } + } + return commitHistoryDiff +} + func (g *GitVersioning) initialize() error { + if g.branchName != "" { + err := g.setReferenceBranch(g.branchName) + if err != nil { + return err + } + commitHistoryCurrentBranch, err := g.git.getCommitHistory() + if err != nil { + return fmt.Errorf("error while retrieving the commit history due to: %w", err) + } + g.commitHistoryCurrentBranch = commitHistoryCurrentBranch + } + err := g.setBranchHead() if err != nil { return err @@ -425,10 +491,14 @@ func (g *GitVersioning) initialize() error { commitHistory, err := g.git.getCommitHistory() if err != nil { - return fmt.Errorf("error while retrieving the commit history due to: %w", err) + return fmt.Errorf("error while retrieving the commit history due to: %w", err) } g.commitHistory = commitHistory + if g.branchName != "" { + g.commitHistoryDiff = g.getCurrentBranchCommitsDiff() + } + mostRecentCommit, err := g.git.getMostRecentCommit() if err != nil { return fmt.Errorf("error while retrieving tags from repository due to: %w", err) @@ -480,7 +550,7 @@ func (v *Version) isGreaterThan(other *Version) bool { return v.Patch > other.Patch } -func New(log Logger, printElapsedTime ElapsedTime, url, username, password, destinationDirectory string) (*GitVersioning, error) { +func New(log Logger, printElapsedTime ElapsedTime, url, username, password, destinationDirectory string, branchName string) (*GitVersioning, error) { gitLabVersioning := &GitVersioning{ log: log, printElapsedTime: printElapsedTime, @@ -494,6 +564,7 @@ func New(log Logger, printElapsedTime ElapsedTime, url, username, password, dest return nil, err } + gitLabVersioning.branchName = branchName repo, err := gitLabVersioning.cloneRepoToDirectory() if err != nil { return nil, fmt.Errorf("error while initiating git package due to : %w", err) @@ -503,6 +574,7 @@ func New(log Logger, printElapsedTime ElapsedTime, url, username, password, dest gitLabVersioning.git = GitMethods{ getBranchPointedToHead: gitLabVersioning.getBranchPointedToHead, + getBranchReference: gitLabVersioning.getBranchReference, getCommitHistory: gitLabVersioning.getCommitHistory, getMostRecentCommit: gitLabVersioning.getMostRecentCommit, getAllTags: gitLabVersioning.getAllTags, diff --git a/src/git/git_mock.go b/src/git/git_mock.go index d5c9d8d..47571dd 100644 --- a/src/git/git_mock.go +++ b/src/git/git_mock.go @@ -76,8 +76,8 @@ func (g *GitVersioning) substituteFunctions(newGit Git) { } func NewMock(log Logger, printElapsedTime ElapsedTime, url, username, password, destinationDirectory string, git Git) (*GitVersioning, error) { - - gitLabVersioning, err := New(log, printElapsedTime, url, username, password, destinationDirectory) + branchName := "" + gitLabVersioning, err := New(log, printElapsedTime, url, username, password, destinationDirectory, branchName) if err != nil { return nil, err } diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 85f2fb2..6d6018f 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -3,15 +3,27 @@ package semantic import ( "errors" "fmt" + "strings" + + "github.com/go-git/go-git/v5/plumbing/object" ) const ( colorCyan = "\033[36m" colorYellow = "\033[33m" colorReset = "\033[0m" + colorRed = "\033[31m" colorBGRed = "\033[41;1;37m" ) +type CommitMessageManager interface { + IsValidMessage(message string) bool +} + +type CommitType interface { + GetCommitChangeType(commitMessage string) (string, error) +} + type Logger interface { Info(s string, args ...interface{}) Error(s string, args ...interface{}) @@ -24,10 +36,11 @@ type RepositoryVersionControl interface { GetChangeMessage() string GetCurrentVersion() string UpgradeRemoteRepository(newVersion string) error + GetCommitHistory() []*object.Commit + GetCommitHistoryDiff() []*object.Commit } type VersionControl interface { - GetCommitChangeType(commitMessage string) (string, error) GetNewVersion(commitMessage string, currentVersion string) (string, error) MustSkipVersioning(commitMessage string) bool } @@ -54,6 +67,8 @@ type Semantic struct { repoVersionControl RepositoryVersionControl versionControl VersionControl filesVersionControl FilesVersionControl + commitMessageManager CommitMessageManager + commitType CommitType } func (s *Semantic) GenerateNewRelease() error { @@ -77,7 +92,7 @@ func (s *Semantic) GenerateNewRelease() error { changesInfo.NewVersion = newVersion - commitChangeType, err := s.versionControl.GetCommitChangeType(changesInfo.Message) + commitChangeType, err := s.commitType.GetCommitChangeType(changesInfo.Message) if err != nil { return fmt.Errorf("error while getting commit change type due to: %s", err.Error()) } @@ -110,7 +125,26 @@ func (s *Semantic) GenerateNewRelease() error { return nil } -func New(log Logger, rootPath string, filesToUpdateVariable interface{}, repoVersionControl RepositoryVersionControl, filesVersionControl FilesVersionControl, versionControl VersionControl) *Semantic { +func (s *Semantic) CommitLint() error { + commitHistoryDiff := s.repoVersionControl.GetCommitHistoryDiff() + areThereWrongCommits := false + for _, commit := range commitHistoryDiff { + if !s.commitMessageManager.IsValidMessage(commit.Message) { + s.log.Error(colorRed+"commit message "+colorYellow+"( %s )"+colorRed+" does not meet semantic-release pattern "+colorYellow+"(type(scope?): message here.)"+colorReset, strings.TrimSuffix(commit.Message, "\n")) + areThereWrongCommits = true + } + } + if areThereWrongCommits { + s.log.Error(colorRed + "You can use " + colorBGRed + "git rebase -i HEAD~" + colorReset + colorRed + " and edit the commit list with reword before each commit message." + colorReset) + return errors.New("commit messages dos not meet semantic-release pattern") + } + + s.log.Info(colorRed + "Remember to adapt the " + colorBGRed + "MERGE REQUEST TITLE" + colorReset + colorRed + " or the " + colorBGRed + "MERGE COMMIT MESSAGE" + colorReset + colorRed + " to semantic-release standards so it can properlly generate the new tag release." + colorReset) + + return nil +} + +func New(log Logger, rootPath string, filesToUpdateVariable interface{}, repoVersionControl RepositoryVersionControl, filesVersionControl FilesVersionControl, versionControl VersionControl, commitMessageManager CommitMessageManager, commitType CommitType) *Semantic { return &Semantic{ log: log, rootPath: rootPath, @@ -118,5 +152,7 @@ func New(log Logger, rootPath string, filesToUpdateVariable interface{}, repoVer repoVersionControl: repoVersionControl, filesVersionControl: filesVersionControl, versionControl: versionControl, + commitMessageManager: commitMessageManager, + commitType: commitType, } } diff --git a/src/semantic/semantic_test.go b/src/semantic/semantic_test.go index 0b7eebc..a77ee59 100644 --- a/src/semantic/semantic_test.go +++ b/src/semantic/semantic_test.go @@ -7,10 +7,15 @@ import ( "errors" "os" "testing" + "time" + commitmessage "github.com/NeowayLabs/semantic-release/src/commit-message" + committype "github.com/NeowayLabs/semantic-release/src/commit-type" "github.com/NeowayLabs/semantic-release/src/log" "github.com/NeowayLabs/semantic-release/src/semantic" "github.com/NeowayLabs/semantic-release/src/tests" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" ) type RepositoryVersionControlMock struct { @@ -21,6 +26,8 @@ type RepositoryVersionControlMock struct { currentVersion string currentChangesInfo changesInfoMock errUpgradeRemoteRepo error + commitHistory []*object.Commit + commitHistoryDiff []*object.Commit } func (r *RepositoryVersionControlMock) GetChangeHash() string { @@ -33,7 +40,7 @@ func (r *RepositoryVersionControlMock) GetChangeAuthorEmail() string { return r.authorEmail } func (r *RepositoryVersionControlMock) GetChangeMessage() string { - return r.message + return r.currentChangesInfo.message } func (r *RepositoryVersionControlMock) GetCurrentVersion() string { return r.currentVersion @@ -43,6 +50,14 @@ func (r *RepositoryVersionControlMock) UpgradeRemoteRepository(newVersion string return r.errUpgradeRemoteRepo } +func (r *RepositoryVersionControlMock) GetCommitHistory() []*object.Commit { + return r.commitHistory +} + +func (r *RepositoryVersionControlMock) GetCommitHistoryDiff() []*object.Commit { + return r.commitHistoryDiff +} + type VersionControlMock struct { newVersion string errGetNewVersion error @@ -93,7 +108,10 @@ func (f *fixture) NewSemantic() *semantic.Semantic { errors.New("error while getting new log") } - return semantic.New(logger, f.rootPath, f.filesToUpdateVariable, f.repoVersionMock, f.filesVersionMock, f.versionControlMock) + commitType := committype.New(logger) + commitMessageManager := commitmessage.New(logger, commitType) + + return semantic.New(logger, f.rootPath, f.filesToUpdateVariable, f.repoVersionMock, f.filesVersionMock, f.versionControlMock, commitMessageManager, commitType) } type upgradeFilesMock struct { @@ -116,12 +134,22 @@ type changesInfoMock struct { changeType string } -func (f *fixture) GetValidChangesInfo() changesInfoMock { +func (f *fixture) GetInvalidTypeMessageChangesInfo() changesInfoMock { + return changesInfoMock{ + hash: "39a757a0", + authorName: "Admin", + authorEmail: "admin@admin.com", + message: "any(scope): Any Message", + currentVersion: "1.0.0", + } +} + +func (f *fixture) GetValidMessageChangesInfo() changesInfoMock { return changesInfoMock{ hash: "39a757a0", authorName: "Admin", authorEmail: "admin@admin.com", - message: "Any Message", + message: "fix(scope): Any Message", currentVersion: "1.0.0", } } @@ -136,6 +164,70 @@ func (f *fixture) GetValidUpgradeFilesInfo() upgradeFilesMock { return upgradeFilesInfo } +func (f *fixture) GetCommitHistoryWithWrongMessagesPattern() []*object.Commit { + var commitHistory []*object.Commit + + author := object.Signature{ + Name: "John Doe", + Email: "john@doe.com", + When: time.Now(), + } + + commitHistory = append(commitHistory, &object.Commit{ + Author: author, + Hash: plumbing.NewHash("anything"), + Committer: author, + PGPSignature: "anything", + Message: "This is a wrong commit message.", + TreeHash: plumbing.NewHash("anything"), + ParentHashes: []plumbing.Hash{plumbing.NewHash("anything")}, + }) + + commitHistory = append(commitHistory, &object.Commit{ + Author: author, + Hash: plumbing.NewHash("anything"), + Committer: author, + PGPSignature: "anything", + Message: "Oh, no! Wrong again.", + TreeHash: plumbing.NewHash("anything"), + ParentHashes: []plumbing.Hash{plumbing.NewHash("anything")}, + }) + + return commitHistory +} + +func (f *fixture) GetCommitHistoryWithRightMessagesPattern() []*object.Commit { + var commitHistory []*object.Commit + + author := object.Signature{ + Name: "John Doe", + Email: "john@doe.com", + When: time.Now(), + } + + commitHistory = append(commitHistory, &object.Commit{ + Author: author, + Hash: plumbing.NewHash("anything"), + Committer: author, + PGPSignature: "anything", + Message: "fix(scope): this is a fix correct commit message.", + TreeHash: plumbing.NewHash("anything"), + ParentHashes: []plumbing.Hash{plumbing.NewHash("anything")}, + }) + + commitHistory = append(commitHistory, &object.Commit{ + Author: author, + Hash: plumbing.NewHash("anything"), + Committer: author, + PGPSignature: "anything", + Message: "feat(scope): this is a feature correct commit message.", + TreeHash: plumbing.NewHash("anything"), + ParentHashes: []plumbing.Hash{plumbing.NewHash("anything")}, + }) + + return commitHistory +} + func TestGenerateNewReleaseMustSkip(t *testing.T) { f := setup() f.versionControlMock.mustSkip = true @@ -148,7 +240,7 @@ func TestGenerateNewReleaseMustSkip(t *testing.T) { func TestGenerateNewReleaseErrorGetNewVersion(t *testing.T) { f := setup() - f.repoVersionMock.currentChangesInfo = f.GetValidChangesInfo() + f.repoVersionMock.currentChangesInfo = f.GetValidMessageChangesInfo() f.versionControlMock.errGetNewVersion = errors.New("get new version error") semanticService := f.NewSemantic() @@ -160,7 +252,8 @@ func TestGenerateNewReleaseErrorGetNewVersion(t *testing.T) { func TestGenerateNewReleaseErrorUpgradeChangeLog(t *testing.T) { f := setup() - f.repoVersionMock.currentChangesInfo = f.GetValidChangesInfo() + f.repoVersionMock.currentChangesInfo = f.GetValidMessageChangesInfo() + f.filesVersionMock.errUpgradeChangeLog = errors.New("upgrade changelog error") semanticService := f.NewSemantic() @@ -172,7 +265,7 @@ func TestGenerateNewReleaseErrorUpgradeChangeLog(t *testing.T) { func TestGenerateNewReleaseErrorUpgradeVariablesInFiles(t *testing.T) { f := setup() - f.repoVersionMock.currentChangesInfo = f.GetValidChangesInfo() + f.repoVersionMock.currentChangesInfo = f.GetValidMessageChangesInfo() f.filesVersionMock.errUpgradeVariableInFiles = errors.New("upgrade variables in files error") f.filesToUpdateVariable = f.GetValidUpgradeFilesInfo() @@ -185,7 +278,7 @@ func TestGenerateNewReleaseErrorUpgradeVariablesInFiles(t *testing.T) { func TestGenerateNewReleaseUpgradeRemoteRepositoryError(t *testing.T) { f := setup() - f.repoVersionMock.currentChangesInfo = f.GetValidChangesInfo() + f.repoVersionMock.currentChangesInfo = f.GetValidMessageChangesInfo() f.repoVersionMock.errUpgradeRemoteRepo = errors.New("upgrade remote repository error") f.filesToUpdateVariable = f.GetValidUpgradeFilesInfo() @@ -198,21 +291,39 @@ func TestGenerateNewReleaseUpgradeRemoteRepositoryError(t *testing.T) { func TestGenerateNewReleaseGetCommitChangeError(t *testing.T) { f := setup() - f.repoVersionMock.currentChangesInfo = f.GetValidChangesInfo() - f.versionControlMock.errCommitChangeType = errors.New("invalid change type") + f.repoVersionMock.currentChangesInfo = f.GetInvalidTypeMessageChangesInfo() + f.versionControlMock.errCommitChangeType = errors.New("change type not found") semanticService := f.NewSemantic() actualErr := semanticService.GenerateNewRelease() tests.AssertError(t, actualErr) - tests.AssertEqualValues(t, "error while getting commit change type due to: invalid change type", actualErr.Error()) + tests.AssertEqualValues(t, "error while getting commit change type due to: change type not found", actualErr.Error()) } func TestGenerateNewReleaseSuccess(t *testing.T) { f := setup() - f.repoVersionMock.currentChangesInfo = f.GetValidChangesInfo() + f.repoVersionMock.currentChangesInfo = f.GetValidMessageChangesInfo() f.filesToUpdateVariable = f.GetValidUpgradeFilesInfo() semanticService := f.NewSemantic() actualErr := semanticService.GenerateNewRelease() tests.AssertNoError(t, actualErr) } + +func TestCommitLintError(t *testing.T) { + f := setup() + f.repoVersionMock.commitHistoryDiff = f.GetCommitHistoryWithWrongMessagesPattern() + + semanticService := f.NewSemantic() + actualErr := semanticService.CommitLint() + tests.AssertError(t, actualErr) +} + +func TestCommitLintSucess(t *testing.T) { + f := setup() + f.repoVersionMock.commitHistoryDiff = f.GetCommitHistoryWithRightMessagesPattern() + + semanticService := f.NewSemantic() + actualErr := semanticService.CommitLint() + tests.AssertNoError(t, actualErr) +} diff --git a/src/tests/asserts.go b/src/tests/asserts.go index 64efda7..57dd0ab 100644 --- a/src/tests/asserts.go +++ b/src/tests/asserts.go @@ -28,6 +28,15 @@ func AssertEqualValues(t *testing.T, expected, actual interface{}) { } } +func AssertDeepEqualValues(t *testing.T, expected, actual interface{}) { + t.Helper() + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Not equal: \n"+ + "expected: %v\n"+ + "actual : %v", expected, actual) + } +} + func AssertDiffValues(t *testing.T, expected, actual interface{}) { t.Helper() if expected == actual { diff --git a/src/version/version.go b/src/version/version.go index 65b7f8c..839f2c1 100644 --- a/src/version/version.go +++ b/src/version/version.go @@ -15,14 +15,6 @@ const ( colorReset = "\033[0m" ) -var ( - commitChangeTypes = []string{"build", "ci", "docs", "fix", "feat", "perf", "refactor", "style", "test", "breaking change", "breaking changes", "skip", "skip versioning", "skip v"} - commitChangeTypesMajorUpgrade = []string{"breaking change", "breaking changes"} - commitChangeTypesMinorUpgrade = []string{"feat"} - commitChangeTypePatchUpgrade = []string{"build", "ci", "docs", "fix", "perf", "refactor", "style", "test"} - commitTypeSkipVersioning = []string{"skip", "skip versioning", "skip v"} -) - type Logger interface { Info(s string, args ...interface{}) Error(s string, args ...interface{}) @@ -30,21 +22,34 @@ type Logger interface { type PrintElapsedTime func(functionName string) func() +type CommitType interface { + GetAll() []string + GetMajorUpgrade() []string + GetMinorUpgrade() []string + GetPatchUpgrade() []string + GetSkipVersioning() []string + GetCommitChangeType(commitMessage string) (string, error) +} + type VersionControl struct { log Logger printElapsedTime PrintElapsedTime + commitType CommitType } // splitVersionMajorMinorPatch get a string version, split it and return a map of int values // Args: -// version (string): Version to be splited. I.e: 2.1.1 +// +// version (string): Version to be splited. I.e: 2.1.1 +// // Returns: -// Success: -// It returns a map of int values -// I.e.: map[MAJOR:2 MINOR:1 PATCH:1] // -// Otherwise: -// error +// Success: +// It returns a map of int values +// I.e.: map[MAJOR:2 MINOR:1 PATCH:1] +// +// Otherwise: +// error func (v *VersionControl) splitVersionMajorMinorPatch(version string) (map[string]int, error) { splitedVersion := strings.Split(version, ".") @@ -74,18 +79,21 @@ func (v *VersionControl) splitVersionMajorMinorPatch(version string) (map[string // getUpgradeType defines where to update the current version // MAJOR.MINOR.PATCH. I.e: 2.1.1 // Args: -// commitChangeType (string): Type of changes within the commit. I.e.: fix, feat, doc, etc. Take a look at CommitChangeTypes variable. +// +// commitChangeType (string): Type of changes within the commit. I.e.: fix, feat, doc, etc. Take a look at CommitChangeTypes variable. +// // Returns: -// MAJOR: if the commit type is in CommitChangeTypesMajorUpgrade slice -// MINOR: if the commit type is in CommitChangeTypesMinorUpgrade slice -// PATCH: if the commit type is in CommitChangeTypePatchUpgrade slice -// Otherwise, it returns an error +// +// MAJOR: if the commit type is in CommitChangeTypesMajorUpgrade slice +// MINOR: if the commit type is in CommitChangeTypesMinorUpgrade slice +// PATCH: if the commit type is in CommitChangeTypePatchUpgrade slice +// Otherwise, it returns an error func (v *VersionControl) getUpgradeType(commitChangeType string) (string, error) { - if v.hasStringInSlice(commitChangeType, commitChangeTypesMajorUpgrade) { + if hasStringInSlice(commitChangeType, v.commitType.GetMajorUpgrade()) { return major, nil - } else if v.hasStringInSlice(commitChangeType, commitChangeTypesMinorUpgrade) { + } else if hasStringInSlice(commitChangeType, v.commitType.GetMinorUpgrade()) { return minor, nil - } else if v.hasStringInSlice(commitChangeType, commitChangeTypePatchUpgrade) { + } else if hasStringInSlice(commitChangeType, v.commitType.GetPatchUpgrade()) { return patch, nil } return "", fmt.Errorf("%s is an invalid upgrade change type", commitChangeType) @@ -93,16 +101,19 @@ func (v *VersionControl) getUpgradeType(commitChangeType string) (string, error) // upgradeVersion upgrade the current version based on the upgradeType. // Args: -// upgradeType (string): MAJOR, MINOR or PATCH. -// currentMajor (string): Current release major version. I.e.: >2<.1.1. -// currentMinor (string): Current release minor version. I.e.: 2.>1<.1. -// currentPatch (string): Current release patch version. I.e.: 2.1.>1<. +// +// upgradeType (string): MAJOR, MINOR or PATCH. +// currentMajor (string): Current release major version. I.e.: >2<.1.1. +// currentMinor (string): Current release minor version. I.e.: 2.>1<.1. +// currentPatch (string): Current release patch version. I.e.: 2.1.>1<. +// // Returns: -// It will return a string with the new version. -// I.e.: -// 1 - If the current version is 2.1.1 and the update type is MAJOR it will return 3.0.0 -// 2 - If the current version is 2.1.1 and the update type is MINOR it will return 2.2.0 -// 1 - If the current version is 2.1.1 and the update type is PATCH it will return 2.1.2 +// +// It will return a string with the new version. +// I.e.: +// 1 - If the current version is 2.1.1 and the update type is MAJOR it will return 3.0.0 +// 2 - If the current version is 2.1.1 and the update type is MINOR it will return 2.2.0 +// 1 - If the current version is 2.1.1 and the update type is PATCH it will return 2.1.2 func (v *VersionControl) upgradeVersion(upgradeType string, currentMajor, currentMinor, currentPatch int) string { versionPattern := "%d.%d.%d" var newVersion string @@ -131,20 +142,23 @@ func (v *VersionControl) isFirstVersion(version string) bool { // GetNewVersion upgrade the current version based on the commitChangeType. // It calls the getUpgradeType function to define where to upgrade the version (MAJOR.MINOR.PATCH). // Args: -// commitMessage (string): The commit message. -// currentVersion (string): Current release version. I.e.: 2.1.1. +// +// commitMessage (string): The commit message. +// currentVersion (string): Current release version. I.e.: 2.1.1. +// // Returns: -// string: It will return a string with the new version. -// I.e.: -// 1 - If the current version is 2.1.1 and the update type is MAJOR it will return 3.0.0 -// 2 - If the current version is 2.1.1 and the update type is MINOR it will return 2.2.0 -// 1 - If the current version is 2.1.1 and the update type is PATCH it will return 2.1.2 -// error: It returns an error when something wrong happen. +// +// string: It will return a string with the new version. +// I.e.: +// 1 - If the current version is 2.1.1 and the update type is MAJOR it will return 3.0.0 +// 2 - If the current version is 2.1.1 and the update type is MINOR it will return 2.2.0 +// 1 - If the current version is 2.1.1 and the update type is PATCH it will return 2.1.2 +// error: It returns an error when something wrong happen. func (v *VersionControl) GetNewVersion(commitMessage string, currentVersion string) (string, error) { defer v.printElapsedTime("GetNewVersion")() v.log.Info("generating new version from %s", currentVersion) - commitChangeType, err := v.GetCommitChangeType(commitMessage) + commitChangeType, err := v.commitType.GetCommitChangeType(commitMessage) if err != nil { return "", fmt.Errorf("error while finding commit change type within commit message due to: %w", err) } @@ -170,33 +184,17 @@ func (v *VersionControl) GetNewVersion(commitMessage string, currentVersion stri return newVersion, nil } -// GetCommitChangeType get the commit type from Message -// I.e.: -// type: [fix] -// message: Commit subject here. -// Output: fix -func (v *VersionControl) GetCommitChangeType(commitMessage string) (string, error) { - v.log.Info("getting commit type from message %s", commitMessage) - splitedMessage := strings.Split(commitMessage, "\n") - for _, row := range splitedMessage { - for _, changeType := range commitChangeTypes { - if strings.Contains(strings.ToLower(row), "type:") && strings.Contains(strings.ToLower(row), fmt.Sprintf("[%s]", changeType)) { - return changeType, nil - } - } - } - - return "", errors.New("change type not found") -} - // hasStringInSlice aims to verify if a string is inside a slice of strings. // It requires a full match. // Args: -// value (string): String value to find. -// slice ([]string): Slice containing strings. +// +// value (string): String value to find. +// slice ([]string): Slice containing strings. +// // Returns: -// bool: True when found, otherwise false. -func (v *VersionControl) hasStringInSlice(value string, slice []string) bool { +// +// bool: True when found, otherwise false. +func hasStringInSlice(value string, slice []string) bool { for i := range slice { if slice[i] == value { return true @@ -207,21 +205,24 @@ func (v *VersionControl) hasStringInSlice(value string, slice []string) bool { // MustSkip compare commit type with skip types (CommitTypeSkipVersioning) to avoid upgrading version. // I.e.: -// commitChangeType: [skip] +// +// commitChangeType: [skip] +// // Output: true func (v *VersionControl) MustSkipVersioning(commitMessage string) bool { - commitChangeType, err := v.GetCommitChangeType(commitMessage) + commitChangeType, err := v.commitType.GetCommitChangeType(commitMessage) if err != nil { return true } - return v.hasStringInSlice(commitChangeType, commitTypeSkipVersioning) + return hasStringInSlice(commitChangeType, v.commitType.GetSkipVersioning()) } // NewVersionControl is the version control constructor -func NewVersionControl(log Logger, printElapsedTime PrintElapsedTime) *VersionControl { +func NewVersionControl(log Logger, printElapsedTime PrintElapsedTime, commitType CommitType) *VersionControl { return &VersionControl{ log: log, printElapsedTime: printElapsedTime, + commitType: commitType, } } diff --git a/src/version/version_test.go b/src/version/version_test.go index 7a1fe46..7adbbd3 100644 --- a/src/version/version_test.go +++ b/src/version/version_test.go @@ -8,6 +8,7 @@ import ( "fmt" "testing" + committype "github.com/NeowayLabs/semantic-release/src/commit-type" "github.com/NeowayLabs/semantic-release/src/log" "github.com/NeowayLabs/semantic-release/src/tests" "github.com/NeowayLabs/semantic-release/src/version" @@ -23,7 +24,8 @@ func setup() *fixture { errors.New("error while getting new log") } - return &fixture{versionControl: version.NewVersionControl(logger, PrintElapsedTimeMock)} + commitType := committype.New(logger) + return &fixture{versionControl: version.NewVersionControl(logger, PrintElapsedTimeMock, commitType)} } func PrintElapsedTimeMock(what string) func() { @@ -42,7 +44,7 @@ func TestGetNewVersionGetCommitChangeTypeFromMessageError(t *testing.T) { func TestGetNewVersionSplitVersionMajorMinorPatchError(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[feat]", "1.0.a") + actualVersion, actualErr := f.versionControl.GetNewVersion("feat(scope): this is the message", "1.0.a") tests.AssertError(t, actualErr) tests.AssertEqualValues(t, "error while spliting version into MAJOR.MINOR.PATCH due to: could not convert a to int", actualErr.Error()) tests.AssertEmpty(t, actualVersion) @@ -50,7 +52,7 @@ func TestGetNewVersionSplitVersionMajorMinorPatchError(t *testing.T) { func TestGetNewVersionSplitVersionPathernError(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[feat]", "1.0") + actualVersion, actualErr := f.versionControl.GetNewVersion("feat(scope): this is the message", "1.0") tests.AssertError(t, actualErr) tests.AssertEqualValues(t, "error while spliting version into MAJOR.MINOR.PATCH due to: version must follow the pattern major.minor.patch. I.e.: 1.0.0", actualErr.Error()) tests.AssertEmpty(t, actualVersion) @@ -58,7 +60,7 @@ func TestGetNewVersionSplitVersionPathernError(t *testing.T) { func TestGetNewVersionGetUpgradeTypeError(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[skip]", "1.0.0") + actualVersion, actualErr := f.versionControl.GetNewVersion("skip(scope): this is the message", "1.0.0") tests.AssertError(t, actualErr) tests.AssertEqualValues(t, "error while getting upgrade type due to: skip is an invalid upgrade change type", actualErr.Error()) tests.AssertEmpty(t, actualVersion) @@ -66,47 +68,107 @@ func TestGetNewVersionGetUpgradeTypeError(t *testing.T) { func TestGetNewVersionMajorSuccess(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[breaking change]", "1.0.0") + actualVersion, actualErr := f.versionControl.GetNewVersion("breaking change(scope): this is the message", "1.0.0") tests.AssertNoError(t, actualErr) tests.AssertEqualValues(t, "2.0.0", actualVersion) } func TestGetNewVersionMinorSuccess(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[feat]", "1.0.0") + actualVersion, actualErr := f.versionControl.GetNewVersion("feat(scope): this is the message", "1.0.0") tests.AssertNoError(t, actualErr) tests.AssertEqualValues(t, "1.1.0", actualVersion) } func TestGetNewVersionPatchSuccess(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[fix]", "1.0.0") + actualVersion, actualErr := f.versionControl.GetNewVersion("fix(scope): this is the message", "1.0.0") tests.AssertNoError(t, actualErr) tests.AssertEqualValues(t, "1.0.1", actualVersion) } func TestMustSkipVersioningFalse(t *testing.T) { f := setup() - actualMustSkip := f.versionControl.MustSkipVersioning("type: [fix]") + actualMustSkip := f.versionControl.MustSkipVersioning("fix(scope): this is the message") tests.AssertEqualValues(t, false, actualMustSkip) } func TestMustSkipVersioningTrue(t *testing.T) { f := setup() - actualMustSkip := f.versionControl.MustSkipVersioning("type: [anything]") + actualMustSkip := f.versionControl.MustSkipVersioning("invalid type(scope): this is the message") tests.AssertEqualValues(t, true, actualMustSkip) - actualMustSkip = f.versionControl.MustSkipVersioning("type: [skip]") + actualMustSkip = f.versionControl.MustSkipVersioning("skip(scope): this is the message") tests.AssertEqualValues(t, true, actualMustSkip) } func TestGetNewVersionFirstVersionSuccess(t *testing.T) { f := setup() - actualVersion, actualErr := f.versionControl.GetNewVersion("type:[fix]", "0.0.0") + actualVersion, actualErr := f.versionControl.GetNewVersion("fix(scope): this is the message", "0.0.0") tests.AssertNoError(t, actualErr) tests.AssertEqualValues(t, "1.0.0", actualVersion) - actualVersion, actualErr = f.versionControl.GetNewVersion("type:[feat]", "0.0.0") + actualVersion, actualErr = f.versionControl.GetNewVersion("feat(scope): this is the message", "0.0.0") tests.AssertNoError(t, actualErr) tests.AssertEqualValues(t, "1.0.0", actualVersion) } + +func TestGetNewVersionFeatTypeSuccess(t *testing.T) { + f := setup() + expected := "1.1.0" + actualVersion, actualErr := f.versionControl.GetNewVersion("feat: this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + + actualVersion, actualErr = f.versionControl.GetNewVersion("feat(default scope): this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) +} + +func TestGetNewVersionAllPatchTypesSuccess(t *testing.T) { + f := setup() + patchTypes := []string{"build", "ci", "docs", "fix", "perf", "refactor", "style", "test"} + expected := "1.0.1" + + for _, versionType := range patchTypes { + actualVersion, actualErr := f.versionControl.GetNewVersion(versionType+": this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + + actualVersion, actualErr = f.versionControl.GetNewVersion(versionType+"(default scope): this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + } +} + +func TestGetNewVersionAllMinorTypesSuccess(t *testing.T) { + f := setup() + minorTypes := []string{"feat"} + expected := "1.1.0" + + for _, versionType := range minorTypes { + actualVersion, actualErr := f.versionControl.GetNewVersion(versionType+": this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + + actualVersion, actualErr = f.versionControl.GetNewVersion(versionType+"(default scope): this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + } +} + +func TestGetNewVersionAllMajorTypesSuccess(t *testing.T) { + f := setup() + majorTypes := []string{"breaking change", "breaking changes"} + expected := "2.0.0" + + for _, versionType := range majorTypes { + actualVersion, actualErr := f.versionControl.GetNewVersion(versionType+": this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + + actualVersion, actualErr = f.versionControl.GetNewVersion(versionType+"(default scope): this is the message", "1.0.0") + tests.AssertNoError(t, actualErr) + tests.AssertEqualValues(t, expected, actualVersion) + } +}