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:
commit
8ecb410edf
@ -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.
|
||||
|
@ -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: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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 ====
|
||||
|
38
sv/git.go
38
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) {
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user