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