diff --git a/README.md b/README.md index 58bc1f7..b57ef85 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,20 @@ download the latest release and add the binary on your path you can config using the environment variables -| Variable | description | default | -| ------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------ | -| MAJOR_VERSION_TYPES | types used to bump major version | | -| MINOR_VERSION_TYPES | types used to bump minor version | feat | -| PATCH_VERSION_TYPES | types used to bump patch version | build,ci,docs,fix,perf,refactor,style,test | -| INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true | -| BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: | -| ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: | -| TAG_PATTERN | tag version pattern | %d.%d.%d | -| RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features | -| VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop | -| COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test | +| Variable | description | default | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | +| MAJOR_VERSION_TYPES | types used to bump major version | | +| MINOR_VERSION_TYPES | types used to bump minor version | feat | +| PATCH_VERSION_TYPES | types used to bump patch version | build,ci,docs,fix,perf,refactor,style,test | +| INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true | +| BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: | +| ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: | +| TAG_PATTERN | tag version pattern | %d.%d.%d | +| RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features | +| VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop | +| COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test | +| ISSUE_KEY_NAME | metadata key name used on validate commit message hook to enhance footer, if blank footer will not be added | jira | +| BRANCH_ISSUE_REGEX | regex to extract issue id from branch name, must have 3 groups (prefix, id, posfix), if blank footer will not be added | ^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)? | ### Running @@ -68,6 +70,26 @@ git-sv rn -h | validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: | | help, h | Shows a list of commands or help for one command | :x: | +##### Use validate-commit-message as prepare-commit-msg hook + +Configure your .git/hooks/prepare-commit-msg + +```bash +#!/bin/sh + +COMMIT_MSG_FILE=$1 +COMMIT_SOURCE=$2 +SHA1=$3 + +git sv vcm --path "$(pwd)" --file $COMMIT_MSG_FILE --source $COMMIT_SOURCE +``` + +tip: you can configure a directory as your global git templates using the command below, check [git config docs](https://git-scm.com/docs/git-config#Documentation/git-config.txt-inittemplateDir) for more information! + +```bash +git config --global init.templatedir '' +``` + ## Development ### Makefile diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index f18f151..f7c9dae 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -18,6 +18,8 @@ type Config struct { ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"` ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"` CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"` + IssueKeyName string `envconfig:"ISSUE_KEY_NAME" default:"jira"` + BranchIssueRegex string `envconfig:"BRANCH_ISSUE_REGEX" default:"^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"` } func loadConfig() Config { diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index 7c53242..25eb19a 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -255,6 +255,9 @@ func validateCommitMessageHandler(git sv.Git, validateMessageProcessor sv.Valida warn("could not enhance commit message, %s", err.Error()) return nil } + if msg == "" { + return nil + } if err := appendOnFile(msg, filepath); err != nil { return fmt.Errorf("failed to append meta-informations on footer, error: %s", err.Error()) diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index e7e727e..fd5b896 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -18,7 +18,7 @@ func main() { semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags) outputFormatter := sv.NewOutputFormatter() - validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes) + validateMessageProcessor := sv.NewValidateMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex) app := cli.NewApp() app.Name = "sv" diff --git a/sv/validatemessage.go b/sv/validatemessage.go index 1ace2f4..5b0e60e 100644 --- a/sv/validatemessage.go +++ b/sv/validatemessage.go @@ -1,6 +1,7 @@ package sv import ( + "bufio" "fmt" "regexp" "strings" @@ -14,17 +15,21 @@ type ValidateMessageProcessor interface { } // NewValidateMessageProcessor ValidateMessageProcessorImpl constructor -func NewValidateMessageProcessor(skipBranches, supportedTypes []string) *ValidateMessageProcessorImpl { +func NewValidateMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex string) *ValidateMessageProcessorImpl { return &ValidateMessageProcessorImpl{ - skipBranches: skipBranches, - supportedTypes: supportedTypes, + skipBranches: skipBranches, + supportedTypes: supportedTypes, + issueKeyName: issueKeyName, + branchIssueRegex: branchIssueRegex, } } // ValidateMessageProcessorImpl process validate message hook. type ValidateMessageProcessorImpl struct { - skipBranches []string - supportedTypes []string + skipBranches []string + supportedTypes []string + issueKeyName string + branchIssueRegex string } // SkipBranch check if branch should be ignored. @@ -46,8 +51,47 @@ func (p ValidateMessageProcessorImpl) Validate(message string) error { // Enhance add metadata on commit message. func (p ValidateMessageProcessorImpl) Enhance(branch string, message string) (string, error) { - //TODO add issue id (branch format on varenv) - return "", nil + if p.branchIssueRegex == "" || p.issueKeyName == "" || hasIssueID(message, p.issueKeyName) { + return "", nil //enhance disabled + } + + r, err := regexp.Compile(p.branchIssueRegex) + if err != nil { + return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error()) + } + + groups := r.FindStringSubmatch(branch) + if len(groups) != 4 { + return "", fmt.Errorf("could not find issue id group with configured regex") + } + + footer := fmt.Sprintf("%s: %s", p.issueKeyName, groups[2]) + + if !hasFooter(message) { + return "\n" + footer, nil + } + + return footer, nil +} + +func hasFooter(message string) bool { + r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^BREAKING CHANGE: .*") + + scanner := bufio.NewScanner(strings.NewReader(message)) + lines := 0 + for scanner.Scan() { + if lines > 0 && r.MatchString(scanner.Text()) { + return true + } + lines++ + } + + return false +} + +func hasIssueID(message, issueKeyName string) bool { + r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName)) + return r.MatchString(message) } func contains(value string, content []string) bool { diff --git a/sv/validatemessage_test.go b/sv/validatemessage_test.go index 2e34160..c852e87 100644 --- a/sv/validatemessage_test.go +++ b/sv/validatemessage_test.go @@ -4,8 +4,46 @@ import ( "testing" ) +var issueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?" + +// messages samples start +var fullMessage = `fix: correct minor typos in code + +see the issue for details + +on typos fixed. + +Reviewed-by: Z +Refs #133` +var fullMessageWithJira = `fix: correct minor typos in code + +see the issue for details + +on typos fixed. + +Reviewed-by: Z +Refs #133 +jira: JIRA-456` +var fullMessageRefs = `fix: correct minor typos in code + +see the issue for details + +on typos fixed. + +Refs #133` +var subjectAndBodyMessage = `fix: correct minor typos in code + +see the issue for details + +on typos fixed.` +var subjectAndFooterMessage = `refactor!: drop support for Node 6 + +BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.` + +// multiline samples end + func TestValidateMessageProcessorImpl_Validate(t *testing.T) { - p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}) + p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex) tests := []struct { name string @@ -37,6 +75,39 @@ func TestValidateMessageProcessorImpl_Validate(t *testing.T) { } } +func TestValidateMessageProcessorImpl_Enhance(t *testing.T) { + p := NewValidateMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", issueRegex) + + tests := []struct { + name string + branch string + message string + want string + wantErr bool + }{ + {"issue on branch name", "JIRA-123", "fix: fix something", "\njira: JIRA-123", false}, + {"issue on branch name with description", "JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false}, + {"issue on branch name with prefix", "feature/JIRA-123", "fix: fix something", "\njira: JIRA-123", false}, + {"with footer", "JIRA-123", fullMessage, "jira: JIRA-123", false}, + {"with issue on footer", "JIRA-123", fullMessageWithJira, "", false}, + {"issue on branch name with prefix and description", "feature/JIRA-123-some-description", "fix: fix something", "\njira: JIRA-123", false}, + {"no issue on branch name", "branch", "fix: fix something", "", true}, + {"unexpected branch name", "feature /JIRA-123", "fix: fix something", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := p.Enhance(tt.branch, tt.message) + if (err != nil) != tt.wantErr { + t.Errorf("ValidateMessageProcessorImpl.Enhance() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ValidateMessageProcessorImpl.Enhance() = %v, want %v", got, tt.want) + } + }) + } +} + func Test_firstLine(t *testing.T) { tests := []struct { name string @@ -58,3 +129,51 @@ func Test_firstLine(t *testing.T) { }) } } + +func Test_hasIssueID(t *testing.T) { + tests := []struct { + name string + message string + issueKeyName string + want bool + }{ + {"single line without issue", "feat: something", "jira", false}, + {"multi line without issue", `feat: something + +yay`, "jira", false}, + {"multi line without jira issue", `feat: something + +jira1: JIRA-123`, "jira", false}, + {"multi line with issue", `feat: something + +jira: JIRA-123`, "jira", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasIssueID(tt.message, tt.issueKeyName); got != tt.want { + t.Errorf("hasIssueID() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_hasFooter(t *testing.T) { + tests := []struct { + name string + message string + want bool + }{ + {"simple message", "feat: add something", false}, + {"full messsage", fullMessage, true}, + {"full messsage with refs", fullMessageRefs, true}, + {"subject and footer message", subjectAndFooterMessage, true}, + {"subject and body message", subjectAndBodyMessage, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hasFooter(tt.message); got != tt.want { + t.Errorf("hasFooter() = %v, want %v", got, tt.want) + } + }) + } +}