0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-11-24 11:10:39 +00:00

Merge pull request #45 from hypervtechnics/feature/commit-message-selector

feat: add commit message selector option
This commit is contained in:
Beatriz Vieira 2022-04-07 22:03:04 -03:00 committed by GitHub
commit 8ecb410edf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 120 additions and 18 deletions

View File

@ -91,6 +91,7 @@ versioning: # versioning bump
tag: tag:
pattern: '%d.%d.%d' # Pattern used to create git 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: release-notes:
# Deprecated!!! please use 'sections' instead! # Deprecated!!! please use 'sections' instead!
@ -121,6 +122,7 @@ branches: # Git branches config.
commit-message: commit-message:
types: [build, ci, chore, docs, feat, fix, perf, refactor, revert, style, test] # Supported commit types. 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: scope:
# Define supported scopes, if blank, scope will not be validated, if not, only scope listed will be valid. # 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. # Don't forget to add "" on your list if you need to define scopes and keep it optional.

View File

@ -77,7 +77,10 @@ func defaultConfig() Config {
UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"}, UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
IgnoreUnknown: false, IgnoreUnknown: false,
}, },
Tag: sv.TagConfig{Pattern: "%d.%d.%d"}, Tag: sv.TagConfig{
Pattern: "%d.%d.%d",
Filter: "",
},
ReleaseNotes: sv.ReleaseNotesConfig{ ReleaseNotes: sv.ReleaseNotesConfig{
Sections: []sv.ReleaseNotesSectionConfig{ Sections: []sv.ReleaseNotesSectionConfig{
{Name: "Features", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"feat"}}, {Name: "Features", SectionType: sv.ReleaseNotesSectionTypeCommits, CommitTypes: []string{"feat"}},
@ -99,6 +102,7 @@ func defaultConfig() Config {
"issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}}, "issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}},
}, },
Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"}, Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
HeaderSelector: "",
}, },
} }
} }

View File

@ -5,6 +5,7 @@ package sv
// CommitMessageConfig config a commit message. // CommitMessageConfig config a commit message.
type CommitMessageConfig struct { type CommitMessageConfig struct {
Types []string `yaml:"types,flow"` Types []string `yaml:"types,flow"`
HeaderSelector string `yaml:"header-selector"`
Scope CommitMessageScopeConfig `yaml:"scope"` Scope CommitMessageScopeConfig `yaml:"scope"`
Footer map[string]CommitMessageFooterConfig `yaml:"footer"` Footer map[string]CommitMessageFooterConfig `yaml:"footer"`
Issue CommitMessageIssueConfig `yaml:"issue"` Issue CommitMessageIssueConfig `yaml:"issue"`
@ -62,6 +63,7 @@ type VersioningConfig struct {
// TagConfig tag preferences. // TagConfig tag preferences.
type TagConfig struct { type TagConfig struct {
Pattern string `yaml:"pattern"` Pattern string `yaml:"pattern"`
Filter string `yaml:"filter"`
} }
// ==== Release Notes ==== // ==== Release Notes ====

View File

@ -15,8 +15,8 @@ import (
) )
const ( const (
logSeparator = "##" logSeparator = "###"
endLine = "~~" endLine = "~~~"
) )
// Git commands. // Git commands.
@ -82,8 +82,8 @@ func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
} }
// LastTag get last tag, if no tag found, return empty. // LastTag get last tag, if no tag found, return empty.
func (GitImpl) LastTag() string { func (g GitImpl) LastTag() string {
cmd := exec.Command("git", "for-each-ref", "refs/tags", "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1") cmd := exec.Command("git", "for-each-ref", "refs/tags/" + g.tagCfg.Filter, "--sort", "-creatordate", "--format", "%(refname:short)", "--count", "1")
out, err := cmd.CombinedOutput() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return "" return ""
@ -114,7 +114,11 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
if err != nil { if err != nil {
return nil, combinedOutputErr(err, out) 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. // Commit runs git commit.
@ -144,7 +148,7 @@ func (g GitImpl) Tag(version semver.Version) (string, error) {
// Tags list repository tags. // Tags list repository tags.
func (g GitImpl) Tags() ([]GitTag, error) { 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() out, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return nil, combinedOutputErr(err, out) return nil, combinedOutputErr(err, out)
@ -188,29 +192,39 @@ func parseTagsOutput(input string) ([]GitTag, error) {
return result, nil 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 := bufio.NewScanner(strings.NewReader(log))
scanner.Split(splitAt([]byte(endLine))) scanner.Split(splitAt([]byte(endLine)))
var logs []GitCommitLog var logs []GitCommitLog
for scanner.Scan() { for scanner.Scan() {
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" { 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) content := strings.Split(strings.Trim(commit, "\""), logSeparator)
timestamp, _ := strconv.Atoi(content[1]) timestamp, _ := strconv.Atoi(content[1])
message, err := messageProcessor.Parse(content[4], content[5])
if err != nil {
return GitCommitLog{}, err
}
return GitCommitLog{ return GitCommitLog{
Date: content[0], Date: content[0],
Timestamp: timestamp, Timestamp: timestamp,
AuthorName: content[2], AuthorName: content[2],
Hash: content[3], 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) { func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {

View File

@ -11,6 +11,7 @@ const (
breakingChangeFooterKey = "BREAKING CHANGE" breakingChangeFooterKey = "BREAKING CHANGE"
breakingChangeMetadataKey = "breaking-change" breakingChangeMetadataKey = "breaking-change"
issueMetadataKey = "issue" issueMetadataKey = "issue"
messageRegexGroupName = "header"
) )
// CommitMessage is a message using conventional commits. // CommitMessage is a message using conventional commits.
@ -55,7 +56,7 @@ type MessageProcessor interface {
Enhance(branch string, message string) (string, error) Enhance(branch string, message string) (string, error)
IssueID(branch string) (string, error) IssueID(branch string) (string, error)
Format(msg CommitMessage) (string, string, string) Format(msg CommitMessage) (string, string, string)
Parse(subject, body string) CommitMessage Parse(subject, body string) (CommitMessage, error)
} }
// NewMessageProcessor MessageProcessorImpl constructor. // NewMessageProcessor MessageProcessorImpl constructor.
@ -80,7 +81,11 @@ func (p MessageProcessorImpl) SkipBranch(branch string, detached bool) bool {
// Validate commit message. // Validate commit message.
func (p MessageProcessorImpl) Validate(message string) error { func (p MessageProcessorImpl) Validate(message string) error {
subject, body := splitCommitMessageContent(message) 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) { if !regexp.MustCompile(`^[a-z+]+(\(.+\))?!?: .+$`).MatchString(subject) {
return fmt.Errorf("subject [%s] should be valid according with conventional commits", 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. // Parse a commit message.
func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage { func (p MessageProcessorImpl) Parse(subject, body string) (CommitMessage, error) {
commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject) preparedSubject, err := p.prepareHeader(subject)
if err != nil {
return CommitMessage{}, err
}
commitType, scope, description, hasBreakingChange := parseSubjectMessage(preparedSubject)
metadata := make(map[string]string) metadata := make(map[string]string)
for key, mdCfg := range p.messageCfg.Footer { for key, mdCfg := range p.messageCfg.Footer {
@ -228,7 +239,31 @@ func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage {
Body: body, Body: body,
IsBreakingChange: hasBreakingChange, IsBreakingChange: hasBreakingChange,
Metadata: metadata, 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) { func parseSubjectMessage(message string) (string, string, string, bool) {

View File

@ -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. // messages samples start.
var fullMessage = `fix: correct minor typos in code var fullMessage = `fix: correct minor typos in code
@ -398,7 +411,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { 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) 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<header>.*)", "Merged PR 123: feat: something", "feat: something", false},
{"matching non-conventional with selector with group", "Merged PR (\\d+): (?P<header>.*)", "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<header>.*)", "something", "", true},
{"matching non-conventional with invalid regex", "Merged PR (\\d+): (?<header>.*)", "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)
}
})
}
}