diff --git a/README.md b/README.md index 6983ba7..ff55c12 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ versioning: # versioning bump tag: pattern: '%d.%d.%d' # Pattern used to create git tag. + filter: '' # Enables you to filter for considerable tags using git pattern syntax release-notes: # Deprecated!!! please use 'sections' instead! @@ -121,6 +122,7 @@ branches: # Git branches config. commit-message: types: [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test] # Supported commit types. + header-selector: '' # You can put in a regex here to select only a certain part of the commit message. Please define a regex group 'header'. scope: # Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid. # Don't forget to add "" on your list if you need to define scopes and keep it optional. diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go index 4bbd0e2..04fc14b 100644 --- a/cmd/git-sv/config.go +++ b/cmd/git-sv/config.go @@ -77,7 +77,10 @@ func defaultConfig() Config { UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"}, IgnoreUnknown: false, }, - Tag: sv.TagConfig{Pattern: "%d.%d.%d"}, + Tag: sv.TagConfig{ + Pattern: "%d.%d.%d", + Filter: "", + }, ReleaseNotes: sv.ReleaseNotesConfig{ Sections: []sv.ReleaseNotesSectionConfig{ {Name: "Features", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"feat"}}, @@ -99,6 +102,7 @@ func defaultConfig() Config { "issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}}, }, Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"}, + HeaderSelector: "", }, } } diff --git a/sv/config.go b/sv/config.go index af19378..264755b 100644 --- a/sv/config.go +++ b/sv/config.go @@ -5,6 +5,7 @@ package sv // CommitMessageConfig config a commit message. type CommitMessageConfig struct { Types []string `yaml:"types,flow"` + HeaderSelector string `yaml:"header-selector"` Scope CommitMessageScopeConfig `yaml:"scope"` Footer map[string]CommitMessageFooterConfig `yaml:"footer"` Issue CommitMessageIssueConfig `yaml:"issue"` @@ -62,6 +63,7 @@ type VersioningConfig struct { // TagConfig tag preferences. type TagConfig struct { Pattern string `yaml:"pattern"` + Filter string `yaml:"filter"` } // ==== Release Notes ==== diff --git a/sv/git.go b/sv/git.go index 7474824..70605e7 100644 --- a/sv/git.go +++ b/sv/git.go @@ -15,8 +15,8 @@ import ( ) const ( - logSeparator = "##" - endLine = "~~" + logSeparator = "###" + endLine = "~~~" ) // Git commands. @@ -82,8 +82,8 @@ func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl { } // LastTag get last tag, if no tag found, return empty. -func (GitImpl) LastTag() string { - cmd := exec.Command("git", "for-each-ref", "refs/tags", "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1") +func (g GitImpl) LastTag() string { + cmd := exec.Command("git", "for-each-ref", "refs/tags/" + g.tagCfg.Filter, "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1") out, err := cmd.CombinedOutput() if err != nil { return "" @@ -114,7 +114,11 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) { if err != nil { return nil, combinedOutputErr(err, out) } - return parseLogOutput(g.messageProcessor, string(out)), nil + logs, parseErr := parseLogOutput(g.messageProcessor, string(out)) + if parseErr != nil { + return nil, parseErr + } + return logs, nil } // Commit runs git commit. @@ -144,7 +148,7 @@ func (g GitImpl) Tag(version semver.Version) (string, error) { // Tags list repository tags. func (g GitImpl) Tags() ([]GitTag, error) { - cmd := exec.Command("git", "for-each-ref", "--sort", "creatordate", "--format", "%(creatordate:iso8601)#%(refname:short)", "refs/tags") + cmd := exec.Command("git", "for-each-ref", "--sort", "creatordate", "--format", "%(creatordate:iso8601)#%(refname:short)", "refs/tags/" + g.tagCfg.Filter) out, err := cmd.CombinedOutput() if err != nil { return nil, combinedOutputErr(err, out) @@ -188,29 +192,39 @@ func parseTagsOutput(input string) ([]GitTag, error) { return result, nil } -func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLog { +func parseLogOutput(messageProcessor MessageProcessor, log string) ([]GitCommitLog, error) { scanner := bufio.NewScanner(strings.NewReader(log)) scanner.Split(splitAt([]byte(endLine))) var logs []GitCommitLog for scanner.Scan() { if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" { - logs = append(logs, parseCommitLog(messageProcessor, text)) + log, err := parseCommitLog(messageProcessor, text) + if err != nil { + return nil, err + } + logs = append(logs, log) } } - return logs + return logs, nil } -func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog { +func parseCommitLog(messageProcessor MessageProcessor, commit string) (GitCommitLog, error) { content := strings.Split(strings.Trim(commit, "\""), logSeparator) timestamp, _ := strconv.Atoi(content[1]) + message, err := messageProcessor.Parse(content[4], content[5]) + + if err != nil { + return GitCommitLog{}, err + } + return GitCommitLog{ Date: content[0], Timestamp: timestamp, AuthorName: content[2], Hash: content[3], - Message: messageProcessor.Parse(content[4], content[5]), - } + Message: message, + }, nil } func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) { diff --git a/sv/message.go b/sv/message.go index 47adec8..fc7e5f7 100644 --- a/sv/message.go +++ b/sv/message.go @@ -11,6 +11,7 @@ const ( breakingChangeFooterKey = "BREAKING CHANGE" breakingChangeMetadataKey = "breaking-change" issueMetadataKey = "issue" + messageRegexGroupName = "header" ) // CommitMessage is a message using conventional commits. @@ -55,7 +56,7 @@ type MessageProcessor interface { Enhance(branch string, message string) (string, error) IssueID(branch string) (string, error) Format(msg CommitMessage) (string, string, string) - Parse(subject, body string) CommitMessage + Parse(subject, body string) (CommitMessage, error) } // NewMessageProcessor MessageProcessorImpl constructor. @@ -80,7 +81,11 @@ func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool { // Validate commit message. func (p MessageProcessorImpl) Validate(message string) error { subject, body := splitCommitMessageContent(message) - msg := p.Parse(subject, body) + msg, parseErr := p.Parse(subject, body) + + if (parseErr != nil) { + return parseErr + } if !regexp.MustCompile(`^[a-z+]+(\(.+\))?!?: .+$`).MatchString(subject) { return fmt.Errorf("subject [%s] should be valid according with conventional commits", subject) @@ -201,8 +206,14 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) } // Parse a commit message. -func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage { - commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject) +func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) { + preparedSubject, err := p.prepareHeader(subject) + + if err != nil { + return CommitMessage{}, err + } + + commitType, scope, description, hasBreakingChange := parseSubjectMessage(preparedSubject) metadata := make(map[string]string) for key, mdCfg := range p.messageCfg.Footer { @@ -228,7 +239,31 @@ func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage { Body: body, IsBreakingChange: hasBreakingChange, Metadata: metadata, + }, nil +} + +func (p MessageProcessorImpl) prepareHeader(header string) (string, error) { + if p.messageCfg.HeaderSelector == "" { + return header, nil } + + regex, err := regexp.Compile(p.messageCfg.HeaderSelector) + if err != nil { + return "", fmt.Errorf("invalid regex on header-selector %s, error: %s", p.messageCfg.HeaderSelector, err.Error()) + } + + index := regex.SubexpIndex(messageRegexGroupName) + if index < 0 { + return "", fmt.Errorf("could not find %s regex group on header-selector regex", messageRegexGroupName) + } + + match := regex.FindStringSubmatch(header) + + if match == nil || len(match) < index { + return "", fmt.Errorf("could not find %s regex group in match result for '%s'", messageRegexGroupName, header) + } + + return match[index], nil } func parseSubjectMessage(message string) (string, string, string, bool) { diff --git a/sv/message_test.go b/sv/message_test.go index e1f95d9..c1e790c 100644 --- a/sv/message_test.go +++ b/sv/message_test.go @@ -62,6 +62,19 @@ func newBranchCfg(skipDetached bool) BranchesConfig { } } +func newCommitMessageCfg(headerSelector string) CommitMessageConfig { + return CommitMessageConfig{ + Types: []string{"feat", "fix"}, + Scope: CommitMessageScopeConfig{Values: []string{"", "scope"}}, + Footer: map[string]CommitMessageFooterConfig{ + "issue": {Key: "jira", KeySynonyms: []string{"Jira"}}, + "refs": {Key: "Refs", UseHash: true}, + }, + Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"}, + HeaderSelector: headerSelector, + } +} + // messages samples start. var fullMessage = `fix: correct minor typos in code @@ -398,7 +411,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) { + if got, err := NewMessageProcessor(tt.cfg, newBranchCfg(false)).Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) && err == nil { t.Errorf("MessageProcessorImpl.Parse() = %v, want %v", got, tt.want) } }) @@ -506,3 +519,35 @@ func Test_parseSubjectMessage(t *testing.T) { }) } } + +func Test_prepareHeader(t *testing.T) { + tests := []struct { + name string + headerSelector string + commitHeader string + wantHeader string + wantError bool + }{ + {"conventional without selector", "", "feat: something", "feat: something", false}, + {"conventional with scope without selector", "", "feat(scope): something", "feat(scope): something", false}, + {"non-conventional without selector", "", "something", "something", false}, + {"matching conventional with selector with group", "Merged PR (\\d+): (?P
.*)", "Merged PR 123: feat: something", "feat: something", false}, + {"matching non-conventional with selector with group", "Merged PR (\\d+): (?P
.*)", "Merged PR 123: something", "something", false}, + {"matching non-conventional with selector without group", "Merged PR (\\d+): (.*)", "Merged PR 123: something", "", true}, + {"non-matching non-conventional with selector with group", "Merged PR (\\d+): (?P
.*)", "something", "", true}, + {"matching non-conventional with invalid regex", "Merged PR (\\d+): (?
.*)", "Merged PR 123: something", "", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msgProcessor := NewMessageProcessor(newCommitMessageCfg(tt.headerSelector), newBranchCfg(false)) + header, err := msgProcessor.prepareHeader(tt.commitHeader) + + if tt.wantError && err == nil { + t.Errorf("prepareHeader() err got = %v, want not nil", err) + } + if header != tt.wantHeader { + t.Errorf("prepareHeader() header got = %v, want %v", header, tt.wantHeader) + } + }) + } +}