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:
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.

View File

@ -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: "",
},
}
}

View File

@ -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 ====

View File

@ -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) {

View File

@ -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) {

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.
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<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)
}
})
}
}