From ed20bb8dfde4137c0b1fffd443816c733c6585a4 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Fri, 23 Feb 2024 17:57:42 -0300 Subject: [PATCH 01/25] Add commit lint CLI option --- cmd/semantic-release/semantic-release.go | 25 ++++-- src/git/git.go | 104 +++++++++++++++++++---- src/git/git_mock.go | 4 +- src/semantic/semantic.go | 35 ++++++++ src/semantic/semantic_test.go | 11 +++ src/version/version.go | 87 +++++++++++-------- 6 files changed, 207 insertions(+), 59 deletions(-) diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index 97601fe..a92b9cc 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -37,6 +37,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 +69,26 @@ 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) - + fmt.Println(*gitHost) + 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) + } + // LINT HERE + logger.Info(colorYellow + "\nSemantic Version commit lint started...\n\n" + colorReset) + semantic.CommitLint() + logger.Info(colorRed + "\nAbout to exit.\n\n" + colorReset) + os.Exit(1) + } + os.Exit(1) 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 +125,7 @@ func addFilesToUpgradeList(upgradePyFile *bool, repositoryRootPath string) Upgra } func validateIncomingParams(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, groupName, projectName, username, password *string, upgradePyFile *bool) { - + fmt.Println(*projectName) 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) @@ -180,7 +193,7 @@ func printCommitMessageExample() { 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.") } -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,7 +201,7 @@ 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()) } diff --git a/src/git/git.go b/src/git/git.go index 8092f87..d1558c8 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) } @@ -387,14 +400,16 @@ 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{ + 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 @@ -429,6 +495,8 @@ func (g *GitVersioning) initialize() error { } g.commitHistory = commitHistory + 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 +548,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 +562,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 +572,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..5d7b6bd 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -3,6 +3,10 @@ package semantic import ( "errors" "fmt" + "os" + "strings" + + "github.com/go-git/go-git/v5/plumbing/object" ) const ( @@ -24,6 +28,8 @@ type RepositoryVersionControl interface { GetChangeMessage() string GetCurrentVersion() string UpgradeRemoteRepository(newVersion string) error + GetCommitHistory() []*object.Commit + GetCommitHistoryDiff() []*object.Commit } type VersionControl interface { @@ -110,6 +116,35 @@ func (s *Semantic) GenerateNewRelease() error { return nil } +func (s *Semantic) isValidMessage(message string) bool { + _, err := s.versionControl.GetCommitChangeType(message) + if err != nil { + if err.Error() == "change type not found" { + s.log.Error("change type not found") + } + return false + } + + return strings.Contains(strings.ToLower(message), ", message:") +} + +func (s *Semantic) CommitLint() error { + commitHistoryDiff := s.repoVersionControl.GetCommitHistoryDiff() + + areThereWrongCommits := false + for _, commit := range commitHistoryDiff { + if !s.isValidMessage(commit.Message) { + s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not follow semantic-release pattern "+colorCyan+"( type: [commit type], message: message here. )"+colorReset, commit.Message) + areThereWrongCommits = true + } + } + if areThereWrongCommits { + os.Exit(1) + } + + return nil +} + func New(log Logger, rootPath string, filesToUpdateVariable interface{}, repoVersionControl RepositoryVersionControl, filesVersionControl FilesVersionControl, versionControl VersionControl) *Semantic { return &Semantic{ log: log, diff --git a/src/semantic/semantic_test.go b/src/semantic/semantic_test.go index 0b7eebc..fb9868d 100644 --- a/src/semantic/semantic_test.go +++ b/src/semantic/semantic_test.go @@ -11,6 +11,7 @@ import ( "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/object" ) type RepositoryVersionControlMock struct { @@ -21,6 +22,8 @@ type RepositoryVersionControlMock struct { currentVersion string currentChangesInfo changesInfoMock errUpgradeRemoteRepo error + commitHistory []*object.Commit + commitHistoryDiff []*object.Commit } func (r *RepositoryVersionControlMock) GetChangeHash() string { @@ -43,6 +46,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 diff --git a/src/version/version.go b/src/version/version.go index 65b7f8c..5685e24 100644 --- a/src/version/version.go +++ b/src/version/version.go @@ -37,14 +37,17 @@ type VersionControl struct { // 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,12 +77,15 @@ 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) { return major, nil @@ -93,16 +99,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,15 +140,18 @@ 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) @@ -172,8 +184,10 @@ func (v *VersionControl) GetNewVersion(commitMessage string, currentVersion stri // GetCommitChangeType get the commit type from Message // I.e.: -// type: [fix] -// message: Commit subject here. +// +// 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) @@ -192,10 +206,13 @@ func (v *VersionControl) GetCommitChangeType(commitMessage string) (string, erro // 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. +// +// bool: True when found, otherwise false. func (v *VersionControl) hasStringInSlice(value string, slice []string) bool { for i := range slice { if slice[i] == value { @@ -207,7 +224,9 @@ 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) From 052689e6be0c4c278e601e84baabce9fbea3b118 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Fri, 23 Feb 2024 18:12:50 -0300 Subject: [PATCH 02/25] Update README.md file with commit-lint usage instructions. --- README.md | 34 +++++++++++++++++++++--- cmd/semantic-release/semantic-release.go | 9 ++++--- src/git/git.go | 8 +++--- src/semantic/semantic.go | 5 ++-- 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index f47e702..b2969b3 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,16 @@ stages: semantic-release: stage: semantic-release + variables: + SEMANTIC_RELEASE_VERSION: latest + dependencies: [] only: refs: - master - before_script: - - docker pull registry.com/dataplatform/semantic-release:latest + 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 +79,30 @@ setup( ) ``` + ### 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: [] + only: + refs: + - master + 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. ``` diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index a92b9cc..aaccab4 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -76,11 +76,12 @@ func main() { if *branchName == "" { logger.Error(colorRed + "\nThe argument -branch-name must be set when --commit-lint is true.\n\n" + colorReset) } - // LINT HERE + logger.Info(colorYellow + "\nSemantic Version commit lint started...\n\n" + colorReset) - semantic.CommitLint() - logger.Info(colorRed + "\nAbout to exit.\n\n" + colorReset) - os.Exit(1) + err := semantic.CommitLint() + if err != nil { + os.Exit(1) + } } os.Exit(1) if err := semantic.GenerateNewRelease(); err != nil { diff --git a/src/git/git.go b/src/git/git.go index d1558c8..9af3a8a 100644 --- a/src/git/git.go +++ b/src/git/git.go @@ -479,7 +479,7 @@ func (g *GitVersioning) initialize() error { } commitHistoryCurrentBranch, 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.commitHistoryCurrentBranch = commitHistoryCurrentBranch } @@ -491,11 +491,13 @@ 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 - g.commitHistoryDiff = g.getCurrentBranchCommitsDiff() + if g.branchName != "" { + g.commitHistoryDiff = g.getCurrentBranchCommitsDiff() + } mostRecentCommit, err := g.git.getMostRecentCommit() if err != nil { diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 5d7b6bd..1765e63 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -3,7 +3,6 @@ package semantic import ( "errors" "fmt" - "os" "strings" "github.com/go-git/go-git/v5/plumbing/object" @@ -134,12 +133,12 @@ func (s *Semantic) CommitLint() error { areThereWrongCommits := false for _, commit := range commitHistoryDiff { if !s.isValidMessage(commit.Message) { - s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not follow semantic-release pattern "+colorCyan+"( type: [commit type], message: message here. )"+colorReset, commit.Message) + s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not meet semantic-release pattern "+colorCyan+"( type: [commit type], message: message here. )"+colorReset, commit.Message) areThereWrongCommits = true } } if areThereWrongCommits { - os.Exit(1) + return errors.New("commit messages dos not meet semantic-release pattern") } return nil From 57e91cbc5600e54e4a1ef99d7eb302244d1b9c71 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 11:03:22 -0300 Subject: [PATCH 03/25] Add tests to git commit message lint --- src/semantic/semantic.go | 2 +- src/semantic/semantic_test.go | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 1765e63..09e7f78 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -124,7 +124,7 @@ func (s *Semantic) isValidMessage(message string) bool { return false } - return strings.Contains(strings.ToLower(message), ", message:") + return strings.Contains(strings.ToLower(message), "message:") } func (s *Semantic) CommitLint() error { diff --git a/src/semantic/semantic_test.go b/src/semantic/semantic_test.go index fb9868d..4a05aaf 100644 --- a/src/semantic/semantic_test.go +++ b/src/semantic/semantic_test.go @@ -7,10 +7,12 @@ import ( "errors" "os" "testing" + "time" "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" ) @@ -147,6 +149,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: "type: [fix], \nmessage: 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: "type: [feat], \nmessage: 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 @@ -227,3 +293,21 @@ func TestGenerateNewReleaseSuccess(t *testing.T) { 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) +} From 85fabe89ad173e2f7e76076e86350e3730180533 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 16:24:28 -0300 Subject: [PATCH 04/25] type: [docs], message: Add pre commit message pattern. --- Makefile | 5 ++++- README.md | 5 +++++ hack/githooks/commit-msg | 23 +++++++++++++++++++++++ hack/set-pre-commit.sh | 5 +++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100755 hack/githooks/commit-msg create mode 100755 hack/set-pre-commit.sh diff --git a/Makefile b/Makefile index 0b17f1a..ca16c8a 100644 --- a/Makefile +++ b/Makefile @@ -123,4 +123,7 @@ githooks: @echo "git hooks copied" shell: modcache imagedev - $(run) sh \ No newline at end of file + $(run) sh + +pre-commit-install: + ./hack/set-pre-commit.sh diff --git a/README.md b/README.md index b2969b3..f18f11b 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,11 @@ commit-lint: - 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} ``` +### Adding pre-commit message lint +``` +make commit-message-install +``` + ### If you need more information about the semantic release CLI usage you can run the following command. ``` diff --git a/hack/githooks/commit-msg b/hack/githooks/commit-msg new file mode 100755 index 0000000..35101eb --- /dev/null +++ b/hack/githooks/commit-msg @@ -0,0 +1,23 @@ +#!/bin/sh +#!/bin/sh +found=0 +for commit_type in 'fix' 'feat' 'docs' 'skip' 'build' 'ci' 'perf' 'refactor' 'style' 'test' 'breaking change' 'breaking changes' 'skip versioning' 'skip v' +do + # I.e.: type: [fix], message: + regex_pattern="type: \[${commit_type}\], message:" + + message=$(head -1 $1) + + check=$(echo "$message" | grep -o -i "$regex_pattern" | wc -l) + + if [ $check = "1" ] ; then + echo "Commit message ok!" + found=1 + break + fi +done +if [ ${found} = 0 ] ; then + echo "Commit message should meet semantic-release pattern." 1>&2 + echo " type: [type-here], message: message here" 1>&2 + exit 1 +fi diff --git a/hack/set-pre-commit.sh b/hack/set-pre-commit.sh new file mode 100755 index 0000000..4b1f0f0 --- /dev/null +++ b/hack/set-pre-commit.sh @@ -0,0 +1,5 @@ +#!/bin/bash +mkdir -p ~/.git-templates/hooks +cp -f ./hack//githooks/commit-msg ~/.git-templates/hooks +chmod +x ~/.git-templates/hooks/commit-msg +git init From 7343d8c17ba25bd20bc6857e88adb04be68d908f Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 16:28:19 -0300 Subject: [PATCH 05/25] type: [docs], message: Add pre commit message pattern. --- hack/githooks/commit-msg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/hack/githooks/commit-msg b/hack/githooks/commit-msg index 35101eb..6b1af49 100755 --- a/hack/githooks/commit-msg +++ b/hack/githooks/commit-msg @@ -19,5 +19,7 @@ done if [ ${found} = 0 ] ; then echo "Commit message should meet semantic-release pattern." 1>&2 echo " type: [type-here], message: message here" 1>&2 + echo "Allowed types:" 1>&2 + echo " ['fix', 'feat', 'docs', 'skip', 'build', 'ci', 'perf', 'refactor', 'style', 'test', 'breaking change', 'breaking changes', 'skip versioning', 'skip v']"1>&2 exit 1 fi From 4d9e08cb077bfa8664bfe9a41625d83e1ca29d4b Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 16:54:21 -0300 Subject: [PATCH 06/25] type: [fix], message: Fix main logic.. --- README.md | 8 ++------ cmd/semantic-release/semantic-release.go | 11 +++++------ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index f18f11b..f8cc02a 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,8 @@ semantic-release: variables: SEMANTIC_RELEASE_VERSION: latest dependencies: [] - only: - refs: - - master + except: + - master before_script: - docker pull registry.com/dataplatform/semantic-release:$SEMANTIC_RELEASE_VERSION script: @@ -94,9 +93,6 @@ commit-lint: variables: SEMANTIC_RELEASE_VERSION: latest dependencies: [] - only: - refs: - - master before_script: - docker pull registry.com/dataplatform/semantic-release:$SEMANTIC_RELEASE_VERSION script: diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index aaccab4..573a89f 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -82,11 +82,11 @@ func main() { if err != nil { os.Exit(1) } - } - os.Exit(1) - if err := semantic.GenerateNewRelease(); err != nil { - logger.Error(err.Error()) - os.Exit(1) + } else { + if err := semantic.GenerateNewRelease(); err != nil { + logger.Error(err.Error()) + os.Exit(1) + } } logger.Info(colorYellow + "\nDone!" + colorReset) @@ -126,7 +126,6 @@ func addFilesToUpgradeList(upgradePyFile *bool, repositoryRootPath string) Upgra } func validateIncomingParams(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, groupName, projectName, username, password *string, upgradePyFile *bool) { - fmt.Println(*projectName) 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) From 5e1e15e3e2f20860aaaad53857c520526b22d582 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 17:26:38 -0300 Subject: [PATCH 07/25] type: [docs], message: Add log with rebase tip. --- src/semantic/semantic.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 09e7f78..0c55265 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -133,11 +133,12 @@ func (s *Semantic) CommitLint() error { areThereWrongCommits := false for _, commit := range commitHistoryDiff { if !s.isValidMessage(commit.Message) { - s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not meet semantic-release pattern "+colorCyan+"( type: [commit type], message: message here. )"+colorReset, commit.Message) + s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not meet semantic-release pattern "+colorCyan+"( type: [commit type], message: message here.)"+colorReset, commit.Message) areThereWrongCommits = true } } if areThereWrongCommits { + s.log.Error(colorYellow + "You can use " + colorBGRed + "git rebase -i HEAD~n " + colorYellow + "and edit commit list with reword before each commit message." + colorReset) return errors.New("commit messages dos not meet semantic-release pattern") } From 0ac7cb342061e54582e56008bf86a18e17239f37 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 21:07:53 -0300 Subject: [PATCH 08/25] type: [fix], message: separate commit message manager package. --- cmd/semantic-release/semantic-release.go | 5 +- src/commit-message/commit_message_manager.go | 91 ++++++++++++++++ .../commit_message_manager_test.go | 52 +++++++++ src/files/files.go | 103 ++++-------------- src/files/files_test.go | 5 +- src/semantic/semantic.go | 2 +- 6 files changed, 176 insertions(+), 82 deletions(-) create mode 100644 src/commit-message/commit_message_manager.go create mode 100644 src/commit-message/commit_message_manager_test.go diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index 573a89f..ad73d54 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -5,6 +5,7 @@ import ( "fmt" "os" + commitmessage "github.com/NeowayLabs/semantic-release/src/commit-message" "github.com/NeowayLabs/semantic-release/src/files" "github.com/NeowayLabs/semantic-release/src/git" "github.com/NeowayLabs/semantic-release/src/log" @@ -206,7 +207,9 @@ func newSemantic(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, grou logger.Fatal(err.Error()) } - filesVersionControl := files.New(logger, timer.PrintElapsedTime, *gitHost, repositoryRootPath, *groupName, *projectName) + commitMessageManager := commitmessage.New(logger) + + filesVersionControl := files.New(logger, timer.PrintElapsedTime, *gitHost, repositoryRootPath, *groupName, *projectName, commitMessageManager) versionControl := v.NewVersionControl(logger, timer.PrintElapsedTime) diff --git a/src/commit-message/commit_message_manager.go b/src/commit-message/commit_message_manager.go new file mode 100644 index 0000000..7493f99 --- /dev/null +++ b/src/commit-message/commit_message_manager.go @@ -0,0 +1,91 @@ +package commitmessage + +import ( + "errors" + "fmt" + "strings" +) + +const ( + messageTag = "message:" +) + +type Logger interface { + Info(s string, args ...interface{}) + Error(s string, args ...interface{}) + Warn(s string, args ...interface{}) +} + +type CommitMessage struct { + log Logger +} + +func (f *CommitMessage) findMessageTag(commitMessage string) bool { + return strings.Contains(strings.ToLower(commitMessage), messageTag) +} + +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:]) +} + +func (f *CommitMessage) 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 +} + +// 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) { + + 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 New(log Logger) *CommitMessage { + return &CommitMessage{ + log: log, + } +} 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..4c2e476 --- /dev/null +++ b/src/commit-message/commit_message_manager_test.go @@ -0,0 +1,52 @@ +//go:build unit +// +build unit + +package commitmessage_test + +import ( + "testing" + + commitMessage "github.com/NeowayLabs/semantic-release/src/commit-message" + "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()) + } + + commitMessageMenager := commitMessage.New(logger) + + return &fixture{log: logger, commitMessageManager: *commitMessageMenager} +} + +func TestPrettifyCommitMessageNoMessageTagError(t *testing.T) { + f := setup(t) + message := "type: [feat], MSG: This is a message without message tag." + prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) + tests.AssertError(t, err) + tests.AssertEmpty(t, prettyMessage) +} + +func TestPrettifyCommitMessageNewLinesSuccess(t *testing.T) { + f := setup(t) + message := "type: [feat] \n\n\n\n\nMessage: This is a message with new lines." + 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 := "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." + 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) +} diff --git a/src/files/files.go b/src/files/files.go index 40a849e..45f3f46 100644 --- a/src/files/files.go +++ b/src/files/files.go @@ -13,7 +13,6 @@ import ( const ( colorYellow = "\033[33m" colorReset = "\033[0m" - messageTag = "message:" ) var ( @@ -26,6 +25,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 +52,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 +70,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)) @@ -195,8 +138,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 +238,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 +303,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..3e06b5d 100644 --- a/src/files/files_test.go +++ b/src/files/files_test.go @@ -7,6 +7,7 @@ import ( "fmt" "testing" + commitmessage "github.com/NeowayLabs/semantic-release/src/commit-message" "github.com/NeowayLabs/semantic-release/src/files" "github.com/NeowayLabs/semantic-release/src/log" "github.com/NeowayLabs/semantic-release/src/tests" @@ -56,7 +57,9 @@ 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) + commitMessageManager := commitmessage.New(f.log) + + return files.New(f.log, printElapsedTimeMock, f.versionControlHost, f.repositoryRootPath, f.groupName, f.projectName, commitMessageManager) } func TestUpgradeVariableInFilesNoError(t *testing.T) { diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 0c55265..467bcb1 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -133,7 +133,7 @@ func (s *Semantic) CommitLint() error { areThereWrongCommits := false for _, commit := range commitHistoryDiff { if !s.isValidMessage(commit.Message) { - s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not meet semantic-release pattern "+colorCyan+"( type: [commit type], message: message here.)"+colorReset, commit.Message) + s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not meet semantic-release pattern "+colorCyan+"( type: [commit type], message: message here.)"+colorReset, strings.TrimSuffix(commit.Message, "\n")) areThereWrongCommits = true } } From b95ee19ed050d312405ede65638e675b1bc3036f Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 21:15:16 -0300 Subject: [PATCH 09/25] type: [fix], message: change error message color --- src/semantic/semantic.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 467bcb1..d8aab62 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -12,6 +12,7 @@ const ( colorCyan = "\033[36m" colorYellow = "\033[33m" colorReset = "\033[0m" + colorRed = "\033[31m" colorBGRed = "\033[41;1;37m" ) @@ -133,12 +134,12 @@ func (s *Semantic) CommitLint() error { areThereWrongCommits := false for _, commit := range commitHistoryDiff { if !s.isValidMessage(commit.Message) { - s.log.Error(colorYellow+"commit message "+colorCyan+"( %s )"+colorYellow+" does not meet semantic-release pattern "+colorCyan+"( type: [commit type], message: message here.)"+colorReset, strings.TrimSuffix(commit.Message, "\n")) + s.log.Error(colorRed+"commit message "+colorYellow+"( %s )"+colorRed+" does not meet semantic-release pattern "+colorYellow+"( type: [commit type], message: message here.)"+colorReset, strings.TrimSuffix(commit.Message, "\n")) areThereWrongCommits = true } } if areThereWrongCommits { - s.log.Error(colorYellow + "You can use " + colorBGRed + "git rebase -i HEAD~n " + colorYellow + "and edit commit list with reword before each commit message." + colorReset) + s.log.Error(colorRed + "You can use " + colorBGRed + "git rebase -i HEAD~n " + colorReset + colorRed + "and edit commit list with reword before each commit message." + colorReset) return errors.New("commit messages dos not meet semantic-release pattern") } From 99d82722287c0e7ef6816e374d8c438a2f1f1743 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Mon, 26 Feb 2024 21:22:22 -0300 Subject: [PATCH 10/25] type: [fix], message: change error message color --- src/semantic/semantic.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index d8aab62..1e70a25 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -139,7 +139,7 @@ func (s *Semantic) CommitLint() error { } } if areThereWrongCommits { - s.log.Error(colorRed + "You can use " + colorBGRed + "git rebase -i HEAD~n " + colorReset + colorRed + "and edit commit list with reword before each commit message." + colorReset) + s.log.Error(colorRed + "You can use " + colorBGRed + "git rebase -i HEAD~n" + 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") } From aba4a053028d65579fd73f77374568cb8d7d4aa0 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Wed, 28 Feb 2024 10:15:52 -0300 Subject: [PATCH 11/25] type: [docs], message: Add git init instruction to the documentation. --- README.md | 5 +++++ hack/set-pre-commit.sh | 1 + 2 files changed, 6 insertions(+) diff --git a/README.md b/README.md index f8cc02a..5ecdb64 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,11 @@ commit-lint: ``` ### Adding pre-commit message lint + +Notes: +1. To activate commit message template in a pre-existing local repository, go to the project root folder and run `git init` command. +2. After `make commit-message-install`, Git will automatically activate commit message template to new cloned projects. + ``` make commit-message-install ``` diff --git a/hack/set-pre-commit.sh b/hack/set-pre-commit.sh index 4b1f0f0..dbe99f7 100755 --- a/hack/set-pre-commit.sh +++ b/hack/set-pre-commit.sh @@ -3,3 +3,4 @@ mkdir -p ~/.git-templates/hooks cp -f ./hack//githooks/commit-msg ~/.git-templates/hooks chmod +x ~/.git-templates/hooks/commit-msg git init +echo "To activate commit message template in a pre-existing local repository, go to the project root folder and run `git init` command." From 2c692f522163f9e724cb189bc8a6577e2bdae5ba Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Wed, 28 Feb 2024 10:21:03 -0300 Subject: [PATCH 12/25] type: [fix], message: remove sensitive log information. --- cmd/semantic-release/semantic-release.go | 1 - src/git/git.go | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index ad73d54..b9a2ba6 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -70,7 +70,6 @@ func main() { case "up": logger.Info(colorYellow + "\nSemantic Version just started the process...\n\n" + colorReset) - fmt.Println(*gitHost) semantic := newSemantic(logger, upgradeVersionCmd, gitHost, groupName, projectName, username, password, upgradePyFile, branchName) if *commitLint { diff --git a/src/git/git.go b/src/git/git.go index 9af3a8a..14d7f1e 100644 --- a/src/git/git.go +++ b/src/git/git.go @@ -399,7 +399,7 @@ 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) + g.log.Info(colorYellow+"cloning current repository "+colorCyan+" %s "+colorYellow+" into "+colorCyan+"%s"+colorReset, g.destinationDirectory) opts := &git.CloneOptions{ Progress: os.Stdout, URL: g.url, From ed3761a2928188b9bfcc41bdc22eb14b3f2de285 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Wed, 28 Feb 2024 11:41:48 -0300 Subject: [PATCH 13/25] type: [docs], message: Add rebase instructions to README.md file. --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 5ecdb64..e260ca4 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,56 @@ setup( install_requires=requirements, packages=find_packages(), ) +``` + + ### Adding pre-commit message lint + +The semantic-release `pre-commit message lint` 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 standards. + +Notes: +1. To activate commit message template in a pre-existing local repository, go to the project root folder and run `git init` command. +2. After `make commit-message-install`, Git will automatically activate commit message template to new cloned projects. + +Run the following command: + +``` +make commit-message-install +``` + +If for some reason you need to skip commit lint, you can run git commit with the `--no-verify` tag as follows. + +``` +git commit -m "type: [typo], message: We do not recommend doing this!" --no-verify +``` + +We do not recommind using `--no-verify` for projects that use [commit lint ci step](#how-to-add-commit-lint-stage-to-gitlab) once it will validate all the branche commit messages. + +If at least one commit message breaks semantic-release standards, you'll have to rebase and reword the wrong commit messages. + + ### 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; + - `-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 @@ -99,16 +143,6 @@ commit-lint: - 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} ``` -### Adding pre-commit message lint - -Notes: -1. To activate commit message template in a pre-existing local repository, go to the project root folder and run `git init` command. -2. After `make commit-message-install`, Git will automatically activate commit message template to new cloned projects. - -``` -make commit-message-install -``` - ### If you need more information about the semantic release CLI usage you can run the following command. ``` From b4ac8132896fb29113c1d6b1105c133e9154eac0 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Wed, 28 Feb 2024 12:30:50 -0300 Subject: [PATCH 14/25] type: [docs], message: Add warn to the end of the commit lint function. --- src/semantic/semantic.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index 1e70a25..73bcb68 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -143,6 +143,8 @@ func (s *Semantic) CommitLint() error { return errors.New("commit messages dos not meet semantic-release pattern") } + s.log.Info(colorRed + "Remember to adapt the " + colorBGRed + "MERGE REQUEST TITLE" + colorRed + " or the " + colorBGRed + "MERGE COMMIT MESSAGE" + colorRed + " to semantic-release standards so it can properlly generate the new tag release." + colorReset) + return nil } From 0ea4fab5929029290cf71bd99d02c3b889ac11e9 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Wed, 28 Feb 2024 16:58:16 -0300 Subject: [PATCH 15/25] type: [docs], message: Add pre-commit installation instructions --- README.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index e260ca4..5469ed3 100644 --- a/README.md +++ b/README.md @@ -78,30 +78,32 @@ setup( ) ``` - ### Adding pre-commit message lint + ### Adding pre-commit message CLI -The semantic-release `pre-commit message lint` 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 standards. +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. -Notes: -1. To activate commit message template in a pre-existing local repository, go to the project root folder and run `git init` command. -2. After `make commit-message-install`, Git will automatically activate commit message template to new cloned projects. +**Requirements** +- [Golang installation](https://go.dev/doc/install) -Run the following command: + +Clone pre-commit project and install it in you SO. ``` -make commit-message-install +git clone git@github.com:NeowayLabs/pre-commit.git ``` -If for some reason you need to skip commit lint, you can run git commit with the `--no-verify` tag as follows. - ``` -git commit -m "type: [typo], message: We do not recommend doing this!" --no-verify +make install ``` -We do not recommind using `--no-verify` for projects that use [commit lint ci step](#how-to-add-commit-lint-stage-to-gitlab) once it will validate all the branche commit messages. +**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. -If at least one commit message breaks semantic-release standards, you'll have to rebase and reword the wrong commit messages. +``` +commit +``` ### How to use `rebase` to rename commit message? From 1fadec4f85ed2bede12447a5fd5d61304e87107e Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Wed, 28 Feb 2024 18:52:58 -0300 Subject: [PATCH 16/25] type: [fix], message: Fix pre-commit documentation on README.md file --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5469ed3..a359d3d 100644 --- a/README.md +++ b/README.md @@ -99,10 +99,10 @@ 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. +After adding new changes with the `git add` command, you can run `commit .` on any git project root path and follow CLI steps. ``` -commit +commit . ``` ### How to use `rebase` to rename commit message? From d294d100cef2f075519224992c3fcc4e1ccf7f8b Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Thu, 29 Feb 2024 19:22:05 -0300 Subject: [PATCH 17/25] breaking change: change semantic release commit message pattern. --- README.md | 23 ++-- cmd/semantic-release/semantic-release.go | 17 ++- src/commit-message/commit_message_manager.go | 76 +++++++----- .../commit_message_manager_test.go | 14 ++- src/commit-type/commit_type.go | 108 ++++++++++++++++++ src/commit-type/commit_type_test.go | 78 +++++++++++++ src/files/files.go | 3 +- src/files/files_test.go | 50 ++------ src/semantic/semantic.go | 36 +++--- src/semantic/semantic_test.go | 44 ++++--- src/tests/asserts.go | 9 ++ src/version/version.go | 54 +++------ src/version/version_test.go | 86 ++++++++++++-- 13 files changed, 426 insertions(+), 172 deletions(-) create mode 100644 src/commit-type/commit_type.go create mode 100644 src/commit-type/commit_type_test.go diff --git a/README.md b/README.md index a359d3d..a5517ea 100644 --- a/README.md +++ b/README.md @@ -161,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. ``` -type: [feat] -message: Added new function to print the Fibonacci sequece. +feat(fibonacci): 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. +The scope is optional, so you can also use the fllowing message standard. -- type: [skip] -- type: [skip v] -- type: [skip versioning] +``` +type: Commit message here. +``` + +I.e. +``` +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 the skip type tags as follows. + +- skip ## Adding new tests diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index b9a2ba6..335a273 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -6,6 +6,7 @@ import ( "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" @@ -80,6 +81,8 @@ func main() { logger.Info(colorYellow + "\nSemantic Version commit lint started...\n\n" + colorReset) err := semantic.CommitLint() if err != nil { + printCommitTypes() + printCommitMessageExample() os.Exit(1) } } else { @@ -186,11 +189,12 @@ func printCommitTypes() { 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, branchName *string) *semantic.Semantic { @@ -206,11 +210,12 @@ func newSemantic(logger *log.Log, upgradeVersionCmd *flag.FlagSet, gitHost, grou logger.Fatal(err.Error()) } - commitMessageManager := commitmessage.New(logger) + 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 index 7493f99..0ea2d06 100644 --- a/src/commit-message/commit_message_manager.go +++ b/src/commit-message/commit_message_manager.go @@ -16,8 +16,18 @@ type Logger interface { Warn(s string, args ...interface{}) } +type CommitType interface { + GetAll() []string + GetMajorUpgrade() []string + GetMinorUpgrade() []string + GetPatchUpgrade() []string + GetSkipVersioning() []string + GetCommitChangeType(commitMessage string) (string, error) +} + type CommitMessage struct { - log Logger + log Logger + commitType CommitType } func (f *CommitMessage) findMessageTag(commitMessage string) bool { @@ -32,21 +42,6 @@ func (f *CommitMessage) upperFirstLetterOfSentence(text string) string { return fmt.Sprintf("%s%s", strings.ToUpper(text[:1]), text[1:]) } -func (f *CommitMessage) 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 -} - // prettifyCommitMessage aims to keep a short message based on the commit message, removing extra information such as commit type. // Args: // @@ -57,24 +52,26 @@ func (f *CommitMessage) getMessage(messageRow string) (string, error) { // string: Returns a commit message with limmited number of characters. // err: Error whenever unexpected issues happen. func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, error) { - - var message string splitedMessage := strings.Split(commitMessage, "\n") + message := "" for _, row := range splitedMessage { - row := strings.ToLower(row) - if f.findMessageTag(row) { + if row == "" { + continue + } + index := strings.Index(row, ":") + commitTypeScope := strings.ToLower(row[:index]) - currentMessage, err := f.getMessage(row) - if err != nil { - return "", fmt.Errorf("error while getting message due to: %w", err) + for _, changeType := range f.commitType.GetAll() { + if strings.Contains(commitTypeScope, changeType) { + index := strings.Index(row, ":") + message = strings.TrimSpace(strings.Replace(row[index:], ":", "", 1)) } - message = currentMessage } } if message == "" { - return "", errors.New("commit message has no tag 'message:'") + return "", errors.New("commit message is empty") } if f.isMessageLongerThanLimit(message) { @@ -84,8 +81,33 @@ func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, err return f.upperFirstLetterOfSentence(message), nil } -func New(log Logger) *CommitMessage { +func (f *CommitMessage) IsValidMessage(message string) bool { + index := strings.Index(message, ":") + + if index == -1 { + 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, + log: log, + commitType: commitType, } } diff --git a/src/commit-message/commit_message_manager_test.go b/src/commit-message/commit_message_manager_test.go index 4c2e476..432dfc0 100644 --- a/src/commit-message/commit_message_manager_test.go +++ b/src/commit-message/commit_message_manager_test.go @@ -7,6 +7,7 @@ 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" ) @@ -22,14 +23,15 @@ func setup(t *testing.T) *fixture { t.Errorf("error while getting log due to %s", err.Error()) } - commitMessageMenager := commitMessage.New(logger) + commitType := committype.New(logger) + commitMessageMenager := commitMessage.New(logger, commitType) return &fixture{log: logger, commitMessageManager: *commitMessageMenager} } -func TestPrettifyCommitMessageNoMessageTagError(t *testing.T) { +func TestPrettifyCommitMessageNoMessageError(t *testing.T) { f := setup(t) - message := "type: [feat], MSG: This is a message without message tag." + message := "feat(scope):" prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) tests.AssertError(t, err) tests.AssertEmpty(t, prettyMessage) @@ -37,7 +39,7 @@ func TestPrettifyCommitMessageNoMessageTagError(t *testing.T) { func TestPrettifyCommitMessageNewLinesSuccess(t *testing.T) { f := setup(t) - message := "type: [feat] \n\n\n\n\nMessage: This is a message with new lines." + message := "feat(scope): This is a message with new lines." prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) tests.AssertNoError(t, err) tests.AssertEqualValues(t, "This is a message with new lines.", prettyMessage) @@ -45,8 +47,8 @@ func TestPrettifyCommitMessageNewLinesSuccess(t *testing.T) { func TestPrettifyCommitMessageCutSuccess(t *testing.T) { f := setup(t) - 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." 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) + 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) } diff --git a/src/commit-type/commit_type.go b/src/commit-type/commit_type.go new file mode 100644 index 0000000..8dff992 --- /dev/null +++ b/src/commit-type/commit_type.go @@ -0,0 +1,108 @@ +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", "perf", "refactor", "style", "test", "breaking change", "breaking changes", "skip"} +} + +func (c *CommitType) GetMajorUpgrade() []string { + return []string{"breaking change", "breaking changes"} +} + +func (c *CommitType) GetMinorUpgrade() []string { + return []string{"feat"} +} + +func (c *CommitType) GetPatchUpgrade() []string { + return []string{"build", "ci", "docs", "fix", "perf", "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 { + if row == "" { + continue + } + index := strings.Index(row, ":") + 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" +} + +// 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 { + if row == "" { + continue + } + index := strings.Index(row, ":") + 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..b0f966a --- /dev/null +++ b/src/commit-type/commit_type_test.go @@ -0,0 +1,78 @@ +//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", "perf", "refactor", "style", "test", "breaking change", "breaking changes", "skip"} + actual := f.commitType.GetAll() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetMajorUpgrade(t *testing.T) { + f := setup(t) + expected := []string{"breaking change", "breaking changes"} + actual := f.commitType.GetMajorUpgrade() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetMinorUpgrade(t *testing.T) { + f := setup(t) + expected := []string{"feat"} + actual := f.commitType.GetMinorUpgrade() + + tests.AssertDeepEqualValues(t, expected, actual) +} + +func TestGetPatchUpgrade(t *testing.T) { + f := setup(t) + expected := []string{"build", "ci", "docs", "fix", "perf", "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) +} diff --git a/src/files/files.go b/src/files/files.go index 45f3f46..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" ) @@ -99,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) } diff --git a/src/files/files_test.go b/src/files/files_test.go index 3e06b5d..1ab69c1 100644 --- a/src/files/files_test.go +++ b/src/files/files_test.go @@ -8,6 +8,7 @@ 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/files" "github.com/NeowayLabs/semantic-release/src/log" "github.com/NeowayLabs/semantic-release/src/tests" @@ -57,7 +58,8 @@ func setup(t *testing.T) *fixture { } func (f *fixture) newFiles() *files.FileVersion { - commitMessageManager := commitmessage.New(f.log) + 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) } @@ -136,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", @@ -157,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", @@ -300,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) { @@ -348,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"} @@ -368,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/semantic/semantic.go b/src/semantic/semantic.go index 73bcb68..b6790f3 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -16,6 +16,14 @@ const ( 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{}) @@ -33,7 +41,6 @@ type RepositoryVersionControl interface { } type VersionControl interface { - GetCommitChangeType(commitMessage string) (string, error) GetNewVersion(commitMessage string, currentVersion string) (string, error) MustSkipVersioning(commitMessage string) bool } @@ -60,6 +67,8 @@ type Semantic struct { repoVersionControl RepositoryVersionControl versionControl VersionControl filesVersionControl FilesVersionControl + commitMessageManager CommitMessageManager + commitType CommitType } func (s *Semantic) GenerateNewRelease() error { @@ -83,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()) } @@ -116,39 +125,26 @@ func (s *Semantic) GenerateNewRelease() error { return nil } -func (s *Semantic) isValidMessage(message string) bool { - _, err := s.versionControl.GetCommitChangeType(message) - if err != nil { - if err.Error() == "change type not found" { - s.log.Error("change type not found") - } - return false - } - - return strings.Contains(strings.ToLower(message), "message:") -} - func (s *Semantic) CommitLint() error { commitHistoryDiff := s.repoVersionControl.GetCommitHistoryDiff() - areThereWrongCommits := false for _, commit := range commitHistoryDiff { - if !s.isValidMessage(commit.Message) { + if !s.commitMessageManager.IsValidMessage(commit.Message) { s.log.Error(colorRed+"commit message "+colorYellow+"( %s )"+colorRed+" does not meet semantic-release pattern "+colorYellow+"( type: [commit type], message: message here.)"+colorReset, strings.TrimSuffix(commit.Message, "\n")) areThereWrongCommits = true } } if areThereWrongCommits { - s.log.Error(colorRed + "You can use " + colorBGRed + "git rebase -i HEAD~n" + colorReset + colorRed + " and edit the commit list with reword before each commit message." + colorReset) + 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" + colorRed + " or the " + colorBGRed + "MERGE COMMIT MESSAGE" + colorRed + " to semantic-release standards so it can properlly generate the new tag release." + colorReset) + 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) *Semantic { +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, @@ -156,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 4a05aaf..a77ee59 100644 --- a/src/semantic/semantic_test.go +++ b/src/semantic/semantic_test.go @@ -9,6 +9,8 @@ import ( "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" @@ -38,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 @@ -106,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 { @@ -129,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", } } @@ -195,7 +210,7 @@ func (f *fixture) GetCommitHistoryWithRightMessagesPattern() []*object.Commit { Hash: plumbing.NewHash("anything"), Committer: author, PGPSignature: "anything", - Message: "type: [fix], \nmessage: this is a fix correct commit message.", + Message: "fix(scope): this is a fix correct commit message.", TreeHash: plumbing.NewHash("anything"), ParentHashes: []plumbing.Hash{plumbing.NewHash("anything")}, }) @@ -205,7 +220,7 @@ func (f *fixture) GetCommitHistoryWithRightMessagesPattern() []*object.Commit { Hash: plumbing.NewHash("anything"), Committer: author, PGPSignature: "anything", - Message: "type: [feat], \nmessage: this is a feature correct commit message.", + Message: "feat(scope): this is a feature correct commit message.", TreeHash: plumbing.NewHash("anything"), ParentHashes: []plumbing.Hash{plumbing.NewHash("anything")}, }) @@ -225,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() @@ -237,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() @@ -249,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() @@ -262,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() @@ -275,18 +291,18 @@ 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() 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 5685e24..2eec06d 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,9 +22,19 @@ 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 @@ -87,11 +89,11 @@ func (v *VersionControl) splitVersionMajorMinorPatch(version string) (map[string // 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 v.hasStringInSlice(commitChangeType, v.commitType.GetMajorUpgrade()) { return major, nil - } else if v.hasStringInSlice(commitChangeType, commitChangeTypesMinorUpgrade) { + } else if v.hasStringInSlice(commitChangeType, v.commitType.GetMinorUpgrade()) { return minor, nil - } else if v.hasStringInSlice(commitChangeType, commitChangeTypePatchUpgrade) { + } else if v.hasStringInSlice(commitChangeType, v.commitType.GetPatchUpgrade()) { return patch, nil } return "", fmt.Errorf("%s is an invalid upgrade change type", commitChangeType) @@ -156,7 +158,7 @@ func (v *VersionControl) GetNewVersion(commitMessage string, currentVersion stri 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) } @@ -182,27 +184,6 @@ 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: @@ -229,18 +210,19 @@ func (v *VersionControl) hasStringInSlice(value string, slice []string) bool { // // 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 v.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) + } +} From fc65df05a2ad450dac8f2bfbc1ea729117323262 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Thu, 29 Feb 2024 19:24:07 -0300 Subject: [PATCH 18/25] fix: fix git_integration_test commit message --- src/git/git_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/git/git_integration_test.go b/src/git/git_integration_test.go index 7b1a845..ca9016e 100644 --- a/src/git/git_integration_test.go +++ b/src/git/git_integration_test.go @@ -101,7 +101,7 @@ func TestNewGitCommitGetChangeMessageNoError(t *testing.T) { tests.AssertNoError(t, err) message := repo.GetChangeMessage() - tests.AssertEqualValues(t, "type: [feat]\r\nmessage: Added requirements.txt file.", message) + tests.AssertEqualValues(t, "feat: Added requirements.txt file.", message) f.cleanLocalRepo(t) } From 6266ce24a15f1064de6916c61d8f4d6771f721ca Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Thu, 29 Feb 2024 19:27:14 -0300 Subject: [PATCH 19/25] fix: remove old pre commit message --- Makefile | 3 --- hack/githooks/commit-msg | 25 ------------------------- hack/set-pre-commit.sh | 6 ------ 3 files changed, 34 deletions(-) delete mode 100755 hack/githooks/commit-msg delete mode 100755 hack/set-pre-commit.sh diff --git a/Makefile b/Makefile index ca16c8a..917d287 100644 --- a/Makefile +++ b/Makefile @@ -124,6 +124,3 @@ githooks: shell: modcache imagedev $(run) sh - -pre-commit-install: - ./hack/set-pre-commit.sh diff --git a/hack/githooks/commit-msg b/hack/githooks/commit-msg deleted file mode 100755 index 6b1af49..0000000 --- a/hack/githooks/commit-msg +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -#!/bin/sh -found=0 -for commit_type in 'fix' 'feat' 'docs' 'skip' 'build' 'ci' 'perf' 'refactor' 'style' 'test' 'breaking change' 'breaking changes' 'skip versioning' 'skip v' -do - # I.e.: type: [fix], message: - regex_pattern="type: \[${commit_type}\], message:" - - message=$(head -1 $1) - - check=$(echo "$message" | grep -o -i "$regex_pattern" | wc -l) - - if [ $check = "1" ] ; then - echo "Commit message ok!" - found=1 - break - fi -done -if [ ${found} = 0 ] ; then - echo "Commit message should meet semantic-release pattern." 1>&2 - echo " type: [type-here], message: message here" 1>&2 - echo "Allowed types:" 1>&2 - echo " ['fix', 'feat', 'docs', 'skip', 'build', 'ci', 'perf', 'refactor', 'style', 'test', 'breaking change', 'breaking changes', 'skip versioning', 'skip v']"1>&2 - exit 1 -fi diff --git a/hack/set-pre-commit.sh b/hack/set-pre-commit.sh deleted file mode 100755 index dbe99f7..0000000 --- a/hack/set-pre-commit.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -mkdir -p ~/.git-templates/hooks -cp -f ./hack//githooks/commit-msg ~/.git-templates/hooks -chmod +x ~/.git-templates/hooks/commit-msg -git init -echo "To activate commit message template in a pre-existing local repository, go to the project root folder and run `git init` command." From 960916e014b9b286ff9103dc02f26db91d2399a7 Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Thu, 29 Feb 2024 19:30:01 -0300 Subject: [PATCH 20/25] fix: remove old pattern messages --- src/git/git.go | 2 +- src/semantic/semantic.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git/git.go b/src/git/git.go index 14d7f1e..8b51b07 100644 --- a/src/git/git.go +++ b/src/git/git.go @@ -301,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 diff --git a/src/semantic/semantic.go b/src/semantic/semantic.go index b6790f3..6d6018f 100644 --- a/src/semantic/semantic.go +++ b/src/semantic/semantic.go @@ -130,7 +130,7 @@ func (s *Semantic) CommitLint() error { 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: [commit type], message: message here.)"+colorReset, strings.TrimSuffix(commit.Message, "\n")) + 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 } } From 5507704a0151710cc3f662045a12248d9df4843b Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Thu, 29 Feb 2024 20:28:55 -0300 Subject: [PATCH 21/25] fix: fix integration tests --- src/git/git.go | 2 +- src/git/git_integration_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/git/git.go b/src/git/git.go index 8b51b07..b9e3402 100644 --- a/src/git/git.go +++ b/src/git/git.go @@ -399,7 +399,7 @@ func (g *GitVersioning) pushTags() error { func (g *GitVersioning) cloneRepoToDirectory() (*git.Repository, error) { defer g.printElapsedTime("CloneRepoToDirectory")() - g.log.Info(colorYellow+"cloning current repository "+colorCyan+" %s "+colorYellow+" into "+colorCyan+"%s"+colorReset, g.destinationDirectory) + g.log.Info(colorYellow+"cloning current repository to "+colorCyan+" %s "+colorReset, g.destinationDirectory) opts := &git.CloneOptions{ Progress: os.Stdout, URL: g.url, diff --git a/src/git/git_integration_test.go b/src/git/git_integration_test.go index ca9016e..7b1a845 100644 --- a/src/git/git_integration_test.go +++ b/src/git/git_integration_test.go @@ -101,7 +101,7 @@ func TestNewGitCommitGetChangeMessageNoError(t *testing.T) { tests.AssertNoError(t, err) message := repo.GetChangeMessage() - tests.AssertEqualValues(t, "feat: Added requirements.txt file.", message) + tests.AssertEqualValues(t, "type: [feat]\r\nmessage: Added requirements.txt file.", message) f.cleanLocalRepo(t) } From 3cba8568771a4b6a5cb2f030e937532e81396fcc Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Fri, 1 Mar 2024 09:43:53 -0300 Subject: [PATCH 22/25] test: add more tests and allow multiple row commit messages --- src/commit-message/commit_message_manager.go | 7 +++-- .../commit_message_manager_test.go | 2 +- src/commit-type/commit_type.go | 15 ++++++++--- src/commit-type/commit_type_test.go | 26 +++++++++++++++++++ 4 files changed, 43 insertions(+), 7 deletions(-) diff --git a/src/commit-message/commit_message_manager.go b/src/commit-message/commit_message_manager.go index 0ea2d06..fa5a3c8 100644 --- a/src/commit-message/commit_message_manager.go +++ b/src/commit-message/commit_message_manager.go @@ -23,6 +23,7 @@ type CommitType interface { GetPatchUpgrade() []string GetSkipVersioning() []string GetCommitChangeType(commitMessage string) (string, error) + IndexNotFound(index int) bool } type CommitMessage struct { @@ -56,10 +57,12 @@ func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, err message := "" for _, row := range splitedMessage { - if row == "" { + index := strings.Index(row, ":") + + if f.commitType.IndexNotFound(index) || row == " " { continue } - index := strings.Index(row, ":") + commitTypeScope := strings.ToLower(row[:index]) for _, changeType := range f.commitType.GetAll() { diff --git a/src/commit-message/commit_message_manager_test.go b/src/commit-message/commit_message_manager_test.go index 432dfc0..a824b5c 100644 --- a/src/commit-message/commit_message_manager_test.go +++ b/src/commit-message/commit_message_manager_test.go @@ -39,7 +39,7 @@ func TestPrettifyCommitMessageNoMessageError(t *testing.T) { func TestPrettifyCommitMessageNewLinesSuccess(t *testing.T) { f := setup(t) - message := "feat(scope): This is a message with new lines." + 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) diff --git a/src/commit-type/commit_type.go b/src/commit-type/commit_type.go index 8dff992..f519add 100644 --- a/src/commit-type/commit_type.go +++ b/src/commit-type/commit_type.go @@ -55,10 +55,11 @@ func (c *CommitType) GetScope(commitMessage string) string { re := regexp.MustCompile(`\((.*?)\)`) for _, row := range splitedMessage { - if row == "" { + index := strings.Index(row, ":") + + if c.IndexNotFound(index) || row == " " { continue } - index := strings.Index(row, ":") commitTypeScope := strings.ToLower(row[:index]) if c.isValidCommitType(commitTypeScope) { @@ -74,6 +75,10 @@ func (c *CommitType) GetScope(commitMessage string) string { return "default" } +func (c *CommitType) IndexNotFound(index int) bool { + return index == -1 +} + // GetCommitChangeType get the commit type from Message // I.e.: // @@ -85,10 +90,12 @@ func (c *CommitType) GetCommitChangeType(commitMessage string) (string, error) { splitedMessage := strings.Split(commitMessage, "\n") for _, row := range splitedMessage { - if row == "" { + index := strings.Index(row, ":") + + if c.IndexNotFound(index) || row == " " { continue } - index := strings.Index(row, ":") + commitTypeScope := strings.ToLower(row[:index]) for _, changeType := range c.GetAll() { diff --git a/src/commit-type/commit_type_test.go b/src/commit-type/commit_type_test.go index b0f966a..dc6e2d4 100644 --- a/src/commit-type/commit_type_test.go +++ b/src/commit-type/commit_type_test.go @@ -76,3 +76,29 @@ func TestGetScopeSuccess(t *testing.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) +} From 1889332a24a9baa4b35fef6ae898e500d3268a4f Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Fri, 1 Mar 2024 10:05:36 -0300 Subject: [PATCH 23/25] test: add more tests --- src/commit-message/commit_message_manager.go | 15 ++----- .../commit_message_manager_test.go | 44 ++++++++++++++++++- src/commit-type/commit_type.go | 2 +- 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/commit-message/commit_message_manager.go b/src/commit-message/commit_message_manager.go index fa5a3c8..1a6298a 100644 --- a/src/commit-message/commit_message_manager.go +++ b/src/commit-message/commit_message_manager.go @@ -6,10 +6,6 @@ import ( "strings" ) -const ( - messageTag = "message:" -) - type Logger interface { Info(s string, args ...interface{}) Error(s string, args ...interface{}) @@ -31,10 +27,6 @@ type CommitMessage struct { commitType CommitType } -func (f *CommitMessage) findMessageTag(commitMessage string) bool { - return strings.Contains(strings.ToLower(commitMessage), messageTag) -} - func (f *CommitMessage) isMessageLongerThanLimit(message string) bool { return len(message) >= 150 } @@ -59,7 +51,7 @@ func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, err for _, row := range splitedMessage { index := strings.Index(row, ":") - if f.commitType.IndexNotFound(index) || row == " " { + if f.commitType.IndexNotFound(index) || row == "" { continue } @@ -67,7 +59,6 @@ func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, err for _, changeType := range f.commitType.GetAll() { if strings.Contains(commitTypeScope, changeType) { - index := strings.Index(row, ":") message = strings.TrimSpace(strings.Replace(row[index:], ":", "", 1)) } } @@ -87,12 +78,12 @@ func (f *CommitMessage) PrettifyCommitMessage(commitMessage string) (string, err func (f *CommitMessage) IsValidMessage(message string) bool { index := strings.Index(message, ":") - if index == -1 { + if f.commitType.IndexNotFound(index) { f.log.Error("commit message out of pattern") return false } - if message == "" || message[index:] == "" { + if message == "" || message[index:] == ":" { f.log.Error("commit message cannot be empty") return false } diff --git a/src/commit-message/commit_message_manager_test.go b/src/commit-message/commit_message_manager_test.go index a824b5c..c935057 100644 --- a/src/commit-message/commit_message_manager_test.go +++ b/src/commit-message/commit_message_manager_test.go @@ -29,7 +29,7 @@ func setup(t *testing.T) *fixture { return &fixture{log: logger, commitMessageManager: *commitMessageMenager} } -func TestPrettifyCommitMessageNoMessageError(t *testing.T) { +func TestPrettifyCommitMessageNoMessageEmptyError(t *testing.T) { f := setup(t) message := "feat(scope):" prettyMessage, err := f.commitMessageManager.PrettifyCommitMessage(message) @@ -52,3 +52,45 @@ func TestPrettifyCommitMessageCutSuccess(t *testing.T) { 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 index f519add..c30a868 100644 --- a/src/commit-type/commit_type.go +++ b/src/commit-type/commit_type.go @@ -92,7 +92,7 @@ func (c *CommitType) GetCommitChangeType(commitMessage string) (string, error) { for _, row := range splitedMessage { index := strings.Index(row, ":") - if c.IndexNotFound(index) || row == " " { + if c.IndexNotFound(index) || row == "" { continue } From e350d666d66efe02eb3cf410a31507726378658b Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Fri, 1 Mar 2024 10:08:39 -0300 Subject: [PATCH 24/25] refactor: version package refactor --- src/version/version.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/version/version.go b/src/version/version.go index 2eec06d..839f2c1 100644 --- a/src/version/version.go +++ b/src/version/version.go @@ -89,11 +89,11 @@ func (v *VersionControl) splitVersionMajorMinorPatch(version string) (map[string // 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, v.commitType.GetMajorUpgrade()) { + if hasStringInSlice(commitChangeType, v.commitType.GetMajorUpgrade()) { return major, nil - } else if v.hasStringInSlice(commitChangeType, v.commitType.GetMinorUpgrade()) { + } else if hasStringInSlice(commitChangeType, v.commitType.GetMinorUpgrade()) { return minor, nil - } else if v.hasStringInSlice(commitChangeType, v.commitType.GetPatchUpgrade()) { + } else if hasStringInSlice(commitChangeType, v.commitType.GetPatchUpgrade()) { return patch, nil } return "", fmt.Errorf("%s is an invalid upgrade change type", commitChangeType) @@ -194,7 +194,7 @@ func (v *VersionControl) GetNewVersion(commitMessage string, currentVersion stri // Returns: // // bool: True when found, otherwise false. -func (v *VersionControl) hasStringInSlice(value string, slice []string) bool { +func hasStringInSlice(value string, slice []string) bool { for i := range slice { if slice[i] == value { return true @@ -215,7 +215,7 @@ func (v *VersionControl) MustSkipVersioning(commitMessage string) bool { return true } - return v.hasStringInSlice(commitChangeType, v.commitType.GetSkipVersioning()) + return hasStringInSlice(commitChangeType, v.commitType.GetSkipVersioning()) } // NewVersionControl is the version control constructor From 0ccb779c14a250e4a0f150df9c5531cdc3940d1a Mon Sep 17 00:00:00 2001 From: Esequiel Virtuoso Date: Fri, 1 Mar 2024 16:51:19 -0300 Subject: [PATCH 25/25] add complete word and abbreviation commit type options --- cmd/semantic-release/semantic-release.go | 9 ++++++--- src/commit-type/commit_type.go | 8 ++++---- src/commit-type/commit_type_test.go | 8 ++++---- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/cmd/semantic-release/semantic-release.go b/cmd/semantic-release/semantic-release.go index 335a273..8b14b2c 100644 --- a/cmd/semantic-release/semantic-release.go +++ b/cmd/semantic-release/semantic-release.go @@ -174,16 +174,19 @@ 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() { diff --git a/src/commit-type/commit_type.go b/src/commit-type/commit_type.go index c30a868..93f6e77 100644 --- a/src/commit-type/commit_type.go +++ b/src/commit-type/commit_type.go @@ -15,19 +15,19 @@ type CommitType struct { } func (c *CommitType) GetAll() []string { - return []string{"build", "ci", "docs", "fix", "feat", "perf", "refactor", "style", "test", "breaking change", "breaking changes", "skip"} + 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{"breaking change", "breaking changes"} + return []string{"bc", "breaking", "breaking change"} } func (c *CommitType) GetMinorUpgrade() []string { - return []string{"feat"} + return []string{"feat", "feature"} } func (c *CommitType) GetPatchUpgrade() []string { - return []string{"build", "ci", "docs", "fix", "perf", "refactor", "style", "test"} + return []string{"build", "ci", "docs", "documentation", "fix", "perf", "performance", "refactor", "style", "test"} } func (c *CommitType) GetSkipVersioning() []string { diff --git a/src/commit-type/commit_type_test.go b/src/commit-type/commit_type_test.go index dc6e2d4..b55c44d 100644 --- a/src/commit-type/commit_type_test.go +++ b/src/commit-type/commit_type_test.go @@ -27,7 +27,7 @@ func setup(t *testing.T) *fixture { func TestGetAll(t *testing.T) { f := setup(t) - expected := []string{"build", "ci", "docs", "fix", "feat", "perf", "refactor", "style", "test", "breaking change", "breaking changes", "skip"} + 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) @@ -35,7 +35,7 @@ func TestGetAll(t *testing.T) { func TestGetMajorUpgrade(t *testing.T) { f := setup(t) - expected := []string{"breaking change", "breaking changes"} + expected := []string{"bc", "breaking", "breaking change"} actual := f.commitType.GetMajorUpgrade() tests.AssertDeepEqualValues(t, expected, actual) @@ -43,7 +43,7 @@ func TestGetMajorUpgrade(t *testing.T) { func TestGetMinorUpgrade(t *testing.T) { f := setup(t) - expected := []string{"feat"} + expected := []string{"feat", "feature"} actual := f.commitType.GetMinorUpgrade() tests.AssertDeepEqualValues(t, expected, actual) @@ -51,7 +51,7 @@ func TestGetMinorUpgrade(t *testing.T) { func TestGetPatchUpgrade(t *testing.T) { f := setup(t) - expected := []string{"build", "ci", "docs", "fix", "perf", "refactor", "style", "test"} + expected := []string{"build", "ci", "docs", "documentation", "fix", "perf", "performance", "refactor", "style", "test"} actual := f.commitType.GetPatchUpgrade() tests.AssertDeepEqualValues(t, expected, actual)