From de23ff963844089b9e095b85b47f46d17e8b4569 Mon Sep 17 00:00:00 2001 From: Beatriz Vieira Date: Sun, 14 Feb 2021 01:04:32 -0300 Subject: [PATCH] refactor: merge CommitMessageProcessor and MessageProcessor --- cmd/git-sv/handlers.go | 2 +- cmd/git-sv/main.go | 8 +- sv/config.go | 38 +++++++++ sv/conventional_commit.go | 121 ---------------------------- sv/conventional_commit_test.go | 60 -------------- sv/git.go | 8 +- sv/message.go | 138 ++++++++++++++++++++++++++------ sv/message_test.go | 140 ++++++++++++++++++++++----------- 8 files changed, 255 insertions(+), 260 deletions(-) create mode 100644 sv/config.go delete mode 100644 sv/conventional_commit.go delete mode 100644 sv/conventional_commit_test.go diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index ccf2f01..8bd9595 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -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) if err != nil { diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 4b93359..1102e1c 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -19,19 +19,19 @@ func main() { // TODO: config using yaml commitMessageCfg := sv.CommitMessageConfig{ Types: cfg.CommitMessageTypes, - Scope: sv.ScopeConfig{}, - Footer: map[string]sv.FooterMetadataConfig{ + Scope: sv.CommitMessageScopeConfig{}, + Footer: map[string]sv.CommitMessageFooterConfig{ "issue": {Key: cfg.IssueIDPrefixes[0], KeySynonyms: cfg.IssueIDPrefixes[1:], Regex: cfg.IssueRegex}, "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) releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags) outputFormatter := sv.NewOutputFormatter() - messageProcessor := sv.NewMessageProcessor(cfg.ValidateMessageSkipBranches, cfg.CommitMessageTypes, cfg.IssueKeyName, cfg.BranchIssueRegex, cfg.IssueRegex) app := cli.NewApp() app.Name = "sv" diff --git a/sv/config.go b/sv/config.go new file mode 100644 index 0000000..5074461 --- /dev/null +++ b/sv/config.go @@ -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 +} diff --git a/sv/conventional_commit.go b/sv/conventional_commit.go deleted file mode 100644 index f15e6a8..0000000 --- a/sv/conventional_commit.go +++ /dev/null @@ -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] -} diff --git a/sv/conventional_commit_test.go b/sv/conventional_commit_test.go deleted file mode 100644 index da87580..0000000 --- a/sv/conventional_commit_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/sv/git.go b/sv/git.go index bffcbe9..3a75c94 100644 --- a/sv/git.go +++ b/sv/git.go @@ -64,12 +64,12 @@ func NewLogRange(t LogRangeType, start, end string) LogRange { // GitImpl git command implementation type GitImpl struct { - messageProcessor CommitMessageProcessor + messageProcessor MessageProcessor tagPattern string } // NewGit constructor -func NewGit(messageProcessor CommitMessageProcessor, tagPattern string) *GitImpl { +func NewGit(messageProcessor MessageProcessor, tagPattern string) *GitImpl { return &GitImpl{ messageProcessor: messageProcessor, tagPattern: tagPattern, @@ -167,7 +167,7 @@ func parseTagsOutput(input string) ([]GitTag, error) { return result, nil } -func parseLogOutput(messageProcessor CommitMessageProcessor, log string) []GitCommitLog { +func parseLogOutput(messageProcessor MessageProcessor, log string) []GitCommitLog { scanner := bufio.NewScanner(strings.NewReader(log)) scanner.Split(splitAt([]byte(endLine))) var logs []GitCommitLog @@ -179,7 +179,7 @@ func parseLogOutput(messageProcessor CommitMessageProcessor, log string) []GitCo return logs } -func parseCommitLog(messageProcessor CommitMessageProcessor, commit string) GitCommitLog { +func parseCommitLog(messageProcessor MessageProcessor, commit string) GitCommitLog { content := strings.Split(strings.Trim(commit, "\""), logSeparator) return GitCommitLog{ diff --git a/sv/message.go b/sv/message.go index 25b75e2..2588f5d 100644 --- a/sv/message.go +++ b/sv/message.go @@ -7,7 +7,43 @@ import ( "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. type MessageProcessor interface { @@ -15,27 +51,24 @@ type MessageProcessor interface { Validate(message string) error Enhance(branch string, message 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 -func NewMessageProcessor(skipBranches, supportedTypes []string, issueKeyName, branchIssueRegex, issueRegex string) *MessageProcessorImpl { +func NewMessageProcessor(cfg CommitMessageConfig, skipBranches []string, branchIssueRegex string) *MessageProcessorImpl { return &MessageProcessorImpl{ + cfg: cfg, skipBranches: skipBranches, - supportedTypes: supportedTypes, - issueKeyName: issueKeyName, branchIssueRegex: branchIssueRegex, - issueRegex: issueRegex, } } // MessageProcessorImpl process validate message hook. type MessageProcessorImpl struct { + cfg CommitMessageConfig skipBranches []string - supportedTypes []string - issueKeyName string branchIssueRegex string - issueRegex string } // SkipBranch check if branch should be ignored. @@ -45,19 +78,19 @@ func (p MessageProcessorImpl) SkipBranch(branch string) bool { // Validate commit message. 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 { return err } 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 } // Enhance add metadata on commit message. 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 } @@ -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") } - 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 } @@ -92,31 +125,84 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) { return groups[2], nil } -// Format format commit message to header, body and footer -func (p MessageProcessorImpl) Format(ctype, scope, subject, body, issue, breakingChanges string) (string, string, string) { +// Format a commit message returning header, body and footer +func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) { var header strings.Builder - header.WriteString(ctype) - if scope != "" { - header.WriteString("(" + scope + ")") + header.WriteString(msg.Type) + if msg.Scope != "" { + header.WriteString("(" + msg.Scope + ")") } header.WriteString(": ") - header.WriteString(subject) + header.WriteString(msg.Description) var footer strings.Builder - if breakingChanges != "" { - footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeKey, breakingChanges)) + if msg.BreakingMessage() != "" { + 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 { 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 + ": .*") scanner := bufio.NewScanner(strings.NewReader(message)) diff --git a/sv/message_test.go b/sv/message_test.go index e14fc02..32e01ad 100644 --- a/sv/message_test.go +++ b/sv/message_test.go @@ -1,9 +1,20 @@ package sv import ( + "reflect" "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 ( branchIssueRegex = "^([a-z]+\\/)?([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 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 { name string @@ -79,7 +90,7 @@ func TestMessageProcessorImpl_Validate(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 { name string @@ -112,7 +123,7 @@ func TestMessageProcessorImpl_Enhance(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 { name string @@ -149,46 +160,6 @@ c` 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) { tests := []struct { name string @@ -252,9 +223,90 @@ func Test_hasFooter(t *testing.T) { } for _, tt := range tests { 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) } }) } } + +// 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) + } + }) + } +}