mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-25 07:30:38 +00:00
refactor: merge CommitMessageProcessor and MessageProcessor
This commit is contained in:
parent
740f05b84a
commit
de23ff9638
@ -290,7 +290,7 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header, body, footer := messageProcessor.Format(ctype.Type, scope, subject, fullBody.String(), issue, breakingChanges)
|
header, body, footer := messageProcessor.Format(sv.NewCommitMessage(ctype.Type, scope, subject, fullBody.String(), issue, breakingChanges))
|
||||||
|
|
||||||
err = git.Commit(header, body, footer)
|
err = git.Commit(header, body, footer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -19,19 +19,19 @@ func main() {
|
|||||||
// TODO: config using yaml
|
// TODO: config using yaml
|
||||||
commitMessageCfg := sv.CommitMessageConfig{
|
commitMessageCfg := sv.CommitMessageConfig{
|
||||||
Types: cfg.CommitMessageTypes,
|
Types: cfg.CommitMessageTypes,
|
||||||
Scope: sv.ScopeConfig{},
|
Scope: sv.CommitMessageScopeConfig{},
|
||||||
Footer: map[string]sv.FooterMetadataConfig{
|
Footer: map[string]sv.CommitMessageFooterConfig{
|
||||||
"issue": {Key: cfg.IssueIDPrefixes[0], KeySynonyms: cfg.IssueIDPrefixes[1:], Regex: cfg.IssueRegex},
|
"issue": {Key: cfg.IssueIDPrefixes[0], KeySynonyms: cfg.IssueIDPrefixes[1:], Regex: cfg.IssueRegex},
|
||||||
"breaking-change": {Key: cfg.BreakingChangePrefixes[0], KeySynonyms: cfg.BreakingChangePrefixes[1:]},
|
"breaking-change": {Key: cfg.BreakingChangePrefixes[0], KeySynonyms: cfg.BreakingChangePrefixes[1:]},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
////
|
////
|
||||||
|
|
||||||
git := sv.NewGit(sv.NewCommitMessageProcessor(commitMessageCfg), cfg.TagPattern)
|
messageProcessor := sv.NewMessageProcessor(commitMessageCfg, cfg.ValidateMessageSkipBranches, cfg.BranchIssueRegex)
|
||||||
|
git := sv.NewGit(messageProcessor, cfg.TagPattern)
|
||||||
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
|
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
|
||||||
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
|
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
|
||||||
outputFormatter := sv.NewOutputFormatter()
|
outputFormatter := sv.NewOutputFormatter()
|
||||||
messageProcessor := sv.NewMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex, cfg.IssueRegex)
|
|
||||||
|
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "sv"
|
app.Name = "sv"
|
||||||
|
38
sv/config.go
Normal file
38
sv/config.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package sv
|
||||||
|
|
||||||
|
// CommitMessageConfig config a commit message.
|
||||||
|
type CommitMessageConfig struct {
|
||||||
|
Types []string
|
||||||
|
Scope CommitMessageScopeConfig
|
||||||
|
Footer map[string]CommitMessageFooterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// IssueConfig config for issue.
|
||||||
|
func (c CommitMessageConfig) IssueConfig() CommitMessageFooterConfig {
|
||||||
|
if v, exists := c.Footer[issueKey]; exists {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return CommitMessageFooterConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BreakingChangeConfig config for breaking changes.
|
||||||
|
func (c CommitMessageConfig) BreakingChangeConfig() CommitMessageFooterConfig {
|
||||||
|
if v, exists := c.Footer[breakingKey]; exists {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return CommitMessageFooterConfig{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitMessageScopeConfig config scope preferences.
|
||||||
|
type CommitMessageScopeConfig struct {
|
||||||
|
Mandatory bool
|
||||||
|
Values []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// CommitMessageFooterConfig config footer metadata.
|
||||||
|
type CommitMessageFooterConfig struct {
|
||||||
|
Key string
|
||||||
|
KeySynonyms []string
|
||||||
|
Regex string
|
||||||
|
UseHash bool
|
||||||
|
}
|
@ -1,121 +0,0 @@
|
|||||||
package sv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
breakingKey = "breaking-change"
|
|
||||||
// IssueIDKey key to issue id metadata
|
|
||||||
issueKey = "issue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CommitMessageConfig config a commit message
|
|
||||||
type CommitMessageConfig struct {
|
|
||||||
Types []string
|
|
||||||
Scope ScopeConfig
|
|
||||||
Footer map[string]FooterMetadataConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// ScopeConfig config scope preferences
|
|
||||||
type ScopeConfig struct {
|
|
||||||
Mandatory bool
|
|
||||||
Values []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// FooterMetadataConfig config footer metadata
|
|
||||||
type FooterMetadataConfig struct {
|
|
||||||
Key string
|
|
||||||
KeySynonyms []string
|
|
||||||
Regex string
|
|
||||||
UseHash bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitMessage is a message using conventional commits.
|
|
||||||
type CommitMessage struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Scope string `json:"scope,omitempty"`
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
Body string `json:"body,omitempty"`
|
|
||||||
IsBreakingChange bool `json:"isBreakingChange,omitempty"`
|
|
||||||
Metadata map[string]string `json:"metadata,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Issue return issue from metadata.
|
|
||||||
func (m CommitMessage) Issue() string {
|
|
||||||
return m.Metadata[issueKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// BreakingMessage return breaking change message from metadata.
|
|
||||||
func (m CommitMessage) BreakingMessage() string {
|
|
||||||
return m.Metadata[breakingKey]
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitMessageProcessor parse commit messages.
|
|
||||||
type CommitMessageProcessor interface {
|
|
||||||
Parse(subject, body string) CommitMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// CommitMessageProcessorImpl commit message processor implementation
|
|
||||||
type CommitMessageProcessorImpl struct {
|
|
||||||
cfg CommitMessageConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCommitMessageProcessor CommitMessageProcessorImpl constructor
|
|
||||||
func NewCommitMessageProcessor(cfg CommitMessageConfig) CommitMessageProcessor {
|
|
||||||
return &CommitMessageProcessorImpl{cfg: cfg}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse parse a commit message
|
|
||||||
func (p CommitMessageProcessorImpl) Parse(subject, body string) CommitMessage {
|
|
||||||
commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject)
|
|
||||||
|
|
||||||
metadata := make(map[string]string)
|
|
||||||
for key, mdCfg := range p.cfg.Footer {
|
|
||||||
prefixes := append([]string{mdCfg.Key}, mdCfg.KeySynonyms...)
|
|
||||||
for _, prefix := range prefixes {
|
|
||||||
if tagValue := extractFooterMetadata(prefix, body, mdCfg.UseHash); tagValue != "" {
|
|
||||||
metadata[key] = tagValue
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, exists := metadata[breakingKey]; exists {
|
|
||||||
hasBreakingChange = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return CommitMessage{
|
|
||||||
Type: commitType,
|
|
||||||
Scope: scope,
|
|
||||||
Description: description,
|
|
||||||
Body: body,
|
|
||||||
IsBreakingChange: hasBreakingChange,
|
|
||||||
Metadata: metadata,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSubjectMessage(message string) (string, string, string, bool) {
|
|
||||||
regex := regexp.MustCompile("([a-z]+)(\\((.*)\\))?(!)?: (.*)")
|
|
||||||
result := regex.FindStringSubmatch(message)
|
|
||||||
if len(result) != 6 {
|
|
||||||
return "", "", message, false
|
|
||||||
}
|
|
||||||
return result[1], result[3], strings.TrimSpace(result[5]), result[4] == "!"
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractFooterMetadata(key, text string, useHash bool) string {
|
|
||||||
var regex *regexp.Regexp
|
|
||||||
if useHash {
|
|
||||||
regex = regexp.MustCompile(key + " (#.*)")
|
|
||||||
} else {
|
|
||||||
regex = regexp.MustCompile(key + ": (.*)")
|
|
||||||
}
|
|
||||||
|
|
||||||
result := regex.FindStringSubmatch(text)
|
|
||||||
if len(result) < 2 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return result[1]
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
package sv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
var cfg = CommitMessageConfig{
|
|
||||||
Types: []string{"feat", "fix"},
|
|
||||||
Scope: ScopeConfig{},
|
|
||||||
Footer: map[string]FooterMetadataConfig{
|
|
||||||
"issue": {Key: "jira", KeySynonyms: []string{"Jira"}, Regex: "[A-Z]+-[0-9]+"},
|
|
||||||
"breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
|
|
||||||
"refs": {Key: "Refs", UseHash: true},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var completeBody = `some descriptions
|
|
||||||
|
|
||||||
jira: JIRA-123
|
|
||||||
BREAKING CHANGE: this change breaks everything`
|
|
||||||
|
|
||||||
var issueOnlyBody = `some descriptions
|
|
||||||
|
|
||||||
jira: JIRA-456`
|
|
||||||
|
|
||||||
var issueSynonymsBody = `some descriptions
|
|
||||||
|
|
||||||
Jira: JIRA-789`
|
|
||||||
|
|
||||||
var hashMetadataBody = `some descriptions
|
|
||||||
|
|
||||||
Jira: JIRA-999
|
|
||||||
Refs #123`
|
|
||||||
|
|
||||||
func TestCommitMessageProcessorImpl_Parse(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
subject string
|
|
||||||
body string
|
|
||||||
want CommitMessage
|
|
||||||
}{
|
|
||||||
{"simple message", "feat: something awesome", "", CommitMessage{Type: "feat", Scope: "", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
|
||||||
{"message with scope", "feat(scope): something awesome", "", CommitMessage{Type: "feat", Scope: "scope", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
|
||||||
{"unmapped type", "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
|
||||||
{"jira and breaking change metadata", "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueKey: "JIRA-123", breakingKey: "this change breaks everything"}}},
|
|
||||||
{"jira only metadata", "feat: something new", issueOnlyBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, Metadata: map[string]string{issueKey: "JIRA-456"}}},
|
|
||||||
{"jira synonyms metadata", "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueKey: "JIRA-789"}}},
|
|
||||||
{"breaking change with exclamation mark", "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}},
|
|
||||||
{"hash metadata", "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueKey: "JIRA-999", "refs": "#123"}}},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
p := NewCommitMessageProcessor(cfg)
|
|
||||||
if got := p.Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) {
|
|
||||||
t.Errorf("CommitMessageProcessorImpl.Parse() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
@ -64,12 +64,12 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
|
|||||||
|
|
||||||
// GitImpl git command implementation
|
// GitImpl git command implementation
|
||||||
type GitImpl struct {
|
type GitImpl struct {
|
||||||
messageProcessor CommitMessageProcessor
|
messageProcessor MessageProcessor
|
||||||
tagPattern string
|
tagPattern string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGit constructor
|
// NewGit constructor
|
||||||
func NewGit(messageProcessor CommitMessageProcessor, tagPattern string) *GitImpl {
|
func NewGit(messageProcessor MessageProcessor, tagPattern string) *GitImpl {
|
||||||
return &GitImpl{
|
return &GitImpl{
|
||||||
messageProcessor: messageProcessor,
|
messageProcessor: messageProcessor,
|
||||||
tagPattern: tagPattern,
|
tagPattern: tagPattern,
|
||||||
@ -167,7 +167,7 @@ func parseTagsOutput(input string) ([]GitTag, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLogOutput(messageProcessor CommitMessageProcessor, log string) []GitCommitLog {
|
func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLog {
|
||||||
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
|
||||||
@ -179,7 +179,7 @@ func parseLogOutput(messageProcessor CommitMessageProcessor, log string) []GitCo
|
|||||||
return logs
|
return logs
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCommitLog(messageProcessor CommitMessageProcessor, commit string) GitCommitLog {
|
func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog {
|
||||||
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
|
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
|
||||||
|
|
||||||
return GitCommitLog{
|
return GitCommitLog{
|
||||||
|
138
sv/message.go
138
sv/message.go
@ -7,7 +7,43 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const breakingChangeKey = "BREAKING CHANGE"
|
const (
|
||||||
|
breakingKey = "breaking-change"
|
||||||
|
// IssueIDKey key to issue id metadata
|
||||||
|
issueKey = "issue"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommitMessage is a message using conventional commits.
|
||||||
|
type CommitMessage struct {
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
IsBreakingChange bool `json:"isBreakingChange,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCommitMessage commit message constructor
|
||||||
|
func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage {
|
||||||
|
metadata := make(map[string]string)
|
||||||
|
if issue != "" {
|
||||||
|
metadata[issueKey] = issue
|
||||||
|
}
|
||||||
|
if breakingChanges != "" {
|
||||||
|
metadata[breakingKey] = breakingChanges
|
||||||
|
}
|
||||||
|
return CommitMessage{Type: ctype, Scope: scope, Description: description, Body: body, IsBreakingChange: breakingChanges != "", Metadata: metadata}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue return issue from metadata.
|
||||||
|
func (m CommitMessage) Issue() string {
|
||||||
|
return m.Metadata[issueKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BreakingMessage return breaking change message from metadata.
|
||||||
|
func (m CommitMessage) BreakingMessage() string {
|
||||||
|
return m.Metadata[breakingKey]
|
||||||
|
}
|
||||||
|
|
||||||
// MessageProcessor interface.
|
// MessageProcessor interface.
|
||||||
type MessageProcessor interface {
|
type MessageProcessor interface {
|
||||||
@ -15,27 +51,24 @@ type MessageProcessor interface {
|
|||||||
Validate(message string) error
|
Validate(message string) error
|
||||||
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(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string)
|
Format(msg CommitMessage) (string, string, string)
|
||||||
|
Parse(subject, body string) CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessageProcessor MessageProcessorImpl constructor
|
// NewMessageProcessor MessageProcessorImpl constructor
|
||||||
func NewMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex, issueRegex string) *MessageProcessorImpl {
|
func NewMessageProcessor(cfg CommitMessageConfig, skipBranches []string, branchIssueRegex string) *MessageProcessorImpl {
|
||||||
return &MessageProcessorImpl{
|
return &MessageProcessorImpl{
|
||||||
|
cfg: cfg,
|
||||||
skipBranches: skipBranches,
|
skipBranches: skipBranches,
|
||||||
supportedTypes: supportedTypes,
|
|
||||||
issueKeyName: issueKeyName,
|
|
||||||
branchIssueRegex: branchIssueRegex,
|
branchIssueRegex: branchIssueRegex,
|
||||||
issueRegex: issueRegex,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageProcessorImpl process validate message hook.
|
// MessageProcessorImpl process validate message hook.
|
||||||
type MessageProcessorImpl struct {
|
type MessageProcessorImpl struct {
|
||||||
|
cfg CommitMessageConfig
|
||||||
skipBranches []string
|
skipBranches []string
|
||||||
supportedTypes []string
|
|
||||||
issueKeyName string
|
|
||||||
branchIssueRegex string
|
branchIssueRegex string
|
||||||
issueRegex string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SkipBranch check if branch should be ignored.
|
// SkipBranch check if branch should be ignored.
|
||||||
@ -45,19 +78,19 @@ func (p MessageProcessorImpl) SkipBranch(branch string) bool {
|
|||||||
|
|
||||||
// Validate commit message.
|
// Validate commit message.
|
||||||
func (p MessageProcessorImpl) Validate(message string) error {
|
func (p MessageProcessorImpl) Validate(message string) error {
|
||||||
valid, err := regexp.MatchString("^("+strings.Join(p.supportedTypes, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
|
valid, err := regexp.MatchString("^("+strings.Join(p.cfg.Types, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !valid {
|
if !valid {
|
||||||
return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.supportedTypes)
|
return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.cfg.Types)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhance add metadata on commit message.
|
// Enhance add metadata on commit message.
|
||||||
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
|
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
|
||||||
if p.branchIssueRegex == "" || p.issueKeyName == "" || hasIssueID(message, p.issueKeyName) {
|
if p.branchIssueRegex == "" || p.cfg.IssueConfig().Key == "" || hasIssueID(message, p.cfg.IssueConfig().Key) {
|
||||||
return "", nil //enhance disabled
|
return "", nil //enhance disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,9 +102,9 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
|
|||||||
return "", fmt.Errorf("could not find issue id using configured regex")
|
return "", fmt.Errorf("could not find issue id using configured regex")
|
||||||
}
|
}
|
||||||
|
|
||||||
footer := fmt.Sprintf("%s: %s", p.issueKeyName, issue)
|
footer := fmt.Sprintf("%s: %s", p.cfg.IssueConfig().Key, issue)
|
||||||
|
|
||||||
if !hasFooter(message) {
|
if !hasFooter(message, p.cfg.Footer[breakingKey].Key) {
|
||||||
return "\n" + footer, nil
|
return "\n" + footer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,31 +125,84 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
|
|||||||
return groups[2], nil
|
return groups[2], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format format commit message to header, body and footer
|
// Format a commit message returning header, body and footer
|
||||||
func (p MessageProcessorImpl) Format(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string) {
|
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
|
||||||
var header strings.Builder
|
var header strings.Builder
|
||||||
header.WriteString(ctype)
|
header.WriteString(msg.Type)
|
||||||
if scope != "" {
|
if msg.Scope != "" {
|
||||||
header.WriteString("(" + scope + ")")
|
header.WriteString("(" + msg.Scope + ")")
|
||||||
}
|
}
|
||||||
header.WriteString(": ")
|
header.WriteString(": ")
|
||||||
header.WriteString(subject)
|
header.WriteString(msg.Description)
|
||||||
|
|
||||||
var footer strings.Builder
|
var footer strings.Builder
|
||||||
if breakingChanges != "" {
|
if msg.BreakingMessage() != "" {
|
||||||
footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeKey, breakingChanges))
|
footer.WriteString(fmt.Sprintf("%s: %s", p.cfg.BreakingChangeConfig().Key, msg.BreakingMessage()))
|
||||||
}
|
}
|
||||||
if issue != "" {
|
if issue, exists := msg.Metadata[issueKey]; exists {
|
||||||
if footer.Len() > 0 {
|
if footer.Len() > 0 {
|
||||||
footer.WriteString("\n")
|
footer.WriteString("\n")
|
||||||
}
|
}
|
||||||
footer.WriteString(fmt.Sprintf("%s: %s", p.issueKeyName, issue))
|
footer.WriteString(fmt.Sprintf("%s: %s", p.cfg.IssueConfig().Key, issue))
|
||||||
}
|
}
|
||||||
|
|
||||||
return header.String(), body, footer.String()
|
return header.String(), msg.Body, footer.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasFooter(message string) bool {
|
// Parse a commit message
|
||||||
|
func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage {
|
||||||
|
commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject)
|
||||||
|
|
||||||
|
metadata := make(map[string]string)
|
||||||
|
for key, mdCfg := range p.cfg.Footer {
|
||||||
|
prefixes := append([]string{mdCfg.Key}, mdCfg.KeySynonyms...)
|
||||||
|
for _, prefix := range prefixes {
|
||||||
|
if tagValue := extractFooterMetadata(prefix, body, mdCfg.UseHash); tagValue != "" {
|
||||||
|
metadata[key] = tagValue
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, exists := metadata[breakingKey]; exists {
|
||||||
|
hasBreakingChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return CommitMessage{
|
||||||
|
Type: commitType,
|
||||||
|
Scope: scope,
|
||||||
|
Description: description,
|
||||||
|
Body: body,
|
||||||
|
IsBreakingChange: hasBreakingChange,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseSubjectMessage(message string) (string, string, string, bool) {
|
||||||
|
regex := regexp.MustCompile("([a-z]+)(\\((.*)\\))?(!)?: (.*)")
|
||||||
|
result := regex.FindStringSubmatch(message)
|
||||||
|
if len(result) != 6 {
|
||||||
|
return "", "", message, false
|
||||||
|
}
|
||||||
|
return result[1], result[3], strings.TrimSpace(result[5]), result[4] == "!"
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFooterMetadata(key, text string, useHash bool) string {
|
||||||
|
var regex *regexp.Regexp
|
||||||
|
if useHash {
|
||||||
|
regex = regexp.MustCompile(key + " (#.*)")
|
||||||
|
} else {
|
||||||
|
regex = regexp.MustCompile(key + ": (.*)")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := regex.FindStringSubmatch(text)
|
||||||
|
if len(result) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return result[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasFooter(message, breakingChangeKey string) bool {
|
||||||
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeKey + ": .*")
|
r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeKey + ": .*")
|
||||||
|
|
||||||
scanner := bufio.NewScanner(strings.NewReader(message))
|
scanner := bufio.NewScanner(strings.NewReader(message))
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
package sv
|
package sv
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cfg = CommitMessageConfig{
|
||||||
|
Types: []string{"feat", "fix"},
|
||||||
|
Scope: CommitMessageScopeConfig{},
|
||||||
|
Footer: map[string]CommitMessageFooterConfig{
|
||||||
|
"issue": {Key: "jira", KeySynonyms: []string{"Jira"}, Regex: "[A-Z]+-[0-9]+"},
|
||||||
|
"breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
|
||||||
|
"refs": {Key: "Refs", UseHash: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
branchIssueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
|
branchIssueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
|
||||||
issueRegex = "[A-Z]+-[0-9]+"
|
issueRegex = "[A-Z]+-[0-9]+"
|
||||||
@ -46,7 +57,7 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
|
|||||||
// multiline samples end
|
// multiline samples end
|
||||||
|
|
||||||
func TestMessageProcessorImpl_Validate(t *testing.T) {
|
func TestMessageProcessorImpl_Validate(t *testing.T) {
|
||||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -79,7 +90,7 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMessageProcessorImpl_Enhance(t *testing.T) {
|
func TestMessageProcessorImpl_Enhance(t *testing.T) {
|
||||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -112,7 +123,7 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMessageProcessorImpl_IssueID(t *testing.T) {
|
func TestMessageProcessorImpl_IssueID(t *testing.T) {
|
||||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -149,46 +160,6 @@ c`
|
|||||||
jira: JIRA-123`
|
jira: JIRA-123`
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageProcessorImpl_Format(t *testing.T) {
|
|
||||||
p := NewMessageProcessor([]string{"develop", "master"}, []string{"feat", "fix"}, "jira", branchIssueRegex, issueRegex)
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
ctype string
|
|
||||||
scope string
|
|
||||||
subject string
|
|
||||||
body string
|
|
||||||
issue string
|
|
||||||
breakingChanges string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
wantHeader string
|
|
||||||
wantBody string
|
|
||||||
wantFooter string
|
|
||||||
}{
|
|
||||||
{"type and subject", args{"feat", "", "subject", "", "", ""}, "feat: subject", "", ""},
|
|
||||||
{"type, scope and subject", args{"feat", "scope", "subject", "", "", ""}, "feat(scope): subject", "", ""},
|
|
||||||
{"type, scope, subject and issue", args{"feat", "scope", "subject", "", "JIRA-123", ""}, "feat(scope): subject", "", "jira: JIRA-123"},
|
|
||||||
{"type, scope, subject and breaking change", args{"feat", "scope", "subject", "", "", "breaks"}, "feat(scope): subject", "", "BREAKING CHANGE: breaks"},
|
|
||||||
{"full message", args{"feat", "scope", "subject", multilineBody, "JIRA-123", "breaks"}, "feat(scope): subject", multilineBody, fullFooter},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
header, body, footer := p.Format(tt.args.ctype, tt.args.scope, tt.args.subject, tt.args.body, tt.args.issue, tt.args.breakingChanges)
|
|
||||||
if header != tt.wantHeader {
|
|
||||||
t.Errorf("MessageProcessorImpl.Format() header = %v, want %v", header, tt.wantHeader)
|
|
||||||
}
|
|
||||||
if body != tt.wantBody {
|
|
||||||
t.Errorf("MessageProcessorImpl.Format() body = %v, want %v", body, tt.wantBody)
|
|
||||||
}
|
|
||||||
if footer != tt.wantFooter {
|
|
||||||
t.Errorf("MessageProcessorImpl.Format() footer = %v, want %v", footer, tt.wantFooter)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Test_firstLine(t *testing.T) {
|
func Test_firstLine(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@ -252,9 +223,90 @@ func Test_hasFooter(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 := hasFooter(tt.message); got != tt.want {
|
if got := hasFooter(tt.message, "BREAKING CHANGE"); got != tt.want {
|
||||||
t.Errorf("hasFooter() = %v, want %v", got, tt.want)
|
t.Errorf("hasFooter() = %v, want %v", got, tt.want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// conventional commit tests
|
||||||
|
|
||||||
|
var completeBody = `some descriptions
|
||||||
|
|
||||||
|
jira: JIRA-123
|
||||||
|
BREAKING CHANGE: this change breaks everything`
|
||||||
|
|
||||||
|
var issueOnlyBody = `some descriptions
|
||||||
|
|
||||||
|
jira: JIRA-456`
|
||||||
|
|
||||||
|
var issueSynonymsBody = `some descriptions
|
||||||
|
|
||||||
|
Jira: JIRA-789`
|
||||||
|
|
||||||
|
var hashMetadataBody = `some descriptions
|
||||||
|
|
||||||
|
Jira: JIRA-999
|
||||||
|
Refs #123`
|
||||||
|
|
||||||
|
func TestMessageProcessorImpl_Parse(t *testing.T) {
|
||||||
|
p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
subject string
|
||||||
|
body string
|
||||||
|
want CommitMessage
|
||||||
|
}{
|
||||||
|
{"simple message", "feat: something awesome", "", CommitMessage{Type: "feat", Scope: "", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||||
|
{"message with scope", "feat(scope): something awesome", "", CommitMessage{Type: "feat", Scope: "scope", Description: "something awesome", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||||
|
{"unmapped type", "unkn: something unknown", "", CommitMessage{Type: "unkn", Scope: "", Description: "something unknown", Body: "", IsBreakingChange: false, Metadata: map[string]string{}}},
|
||||||
|
{"jira and breaking change metadata", "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueKey: "JIRA-123", breakingKey: "this change breaks everything"}}},
|
||||||
|
{"jira only metadata", "feat: something new", issueOnlyBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueOnlyBody, IsBreakingChange: false, Metadata: map[string]string{issueKey: "JIRA-456"}}},
|
||||||
|
{"jira synonyms metadata", "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueKey: "JIRA-789"}}},
|
||||||
|
{"breaking change with exclamation mark", "feat!: something new", "", CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: "", IsBreakingChange: true, Metadata: map[string]string{}}},
|
||||||
|
{"hash metadata", "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueKey: "JIRA-999", "refs": "#123"}}},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := p.Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) {
|
||||||
|
t.Errorf("MessageProcessorImpl.Parse() = %v, want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageProcessorImpl_Format(t *testing.T) {
|
||||||
|
p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
msg CommitMessage
|
||||||
|
wantHeader string
|
||||||
|
wantBody string
|
||||||
|
wantFooter string
|
||||||
|
}{
|
||||||
|
{"simple message", NewCommitMessage("feat", "", "something", "", "", ""), "feat: something", "", ""},
|
||||||
|
{"with issue", NewCommitMessage("feat", "", "something", "", "JIRA-123", ""), "feat: something", "", "jira: JIRA-123"},
|
||||||
|
{"with breaking change", NewCommitMessage("feat", "", "something", "", "", "breaks"), "feat: something", "", "BREAKING CHANGE: breaks"},
|
||||||
|
{"with scope", NewCommitMessage("feat", "scope", "something", "", "", ""), "feat(scope): something", "", ""},
|
||||||
|
{"with body", NewCommitMessage("feat", "", "something", "body", "", ""), "feat: something", "body", ""},
|
||||||
|
{"with multiline body", NewCommitMessage("feat", "", "something", multilineBody, "", ""), "feat: something", multilineBody, ""},
|
||||||
|
{"full message", NewCommitMessage("feat", "scope", "something", multilineBody, "JIRA-123", "breaks"), "feat(scope): something", multilineBody, fullFooter},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got, got1, got2 := p.Format(tt.msg)
|
||||||
|
if got != tt.wantHeader {
|
||||||
|
t.Errorf("MessageProcessorImpl.Format() header got = %v, want %v", got, tt.wantHeader)
|
||||||
|
}
|
||||||
|
if got1 != tt.wantBody {
|
||||||
|
t.Errorf("MessageProcessorImpl.Format() body got = %v, want %v", got1, tt.wantBody)
|
||||||
|
}
|
||||||
|
if got2 != tt.wantFooter {
|
||||||
|
t.Errorf("MessageProcessorImpl.Format() footer got = %v, want %v", got2, tt.wantFooter)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user