From 8cf6f1eb56c4ab474e6734e334fc67608045d730 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sat, 13 Feb 2021 23:35:31 -0300
Subject: [PATCH 01/24] feat: support conventional commits exclamation mark on
breaking changes
BREAKING CHANGE: changes commit-log command json, rename subject to description, move all commit message attributes to 'message'
---
cmd/git-sv/config.go | 4 +-
cmd/git-sv/main.go | 13 +++-
sv/conventional_commit.go | 121 +++++++++++++++++++++++++++++++++
sv/conventional_commit_test.go | 60 ++++++++++++++++
sv/formatter.go | 2 +-
sv/git.go | 73 ++++----------------
sv/helpers_test.go | 13 +++-
sv/releasenotes.go | 11 +--
sv/releasenotes_test.go | 2 +-
sv/semver.go | 8 +--
sv/semver_test.go | 2 +-
11 files changed, 233 insertions(+), 76 deletions(-)
create mode 100644 sv/conventional_commit.go
create mode 100644 sv/conventional_commit_test.go
diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go
index 51421bb..3f8d454 100644
--- a/cmd/git-sv/config.go
+++ b/cmd/git-sv/config.go
@@ -12,8 +12,8 @@ type Config struct {
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,chore,docs,fix,perf,refactor,style,test"`
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
- BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE:,BREAKING CHANGES:"`
- IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira:,JIRA:,Jira:"`
+ BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE,BREAKING CHANGES"`
+ IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira,JIRA,Jira"`
TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"`
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index 43894ee..b1ad572 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -16,7 +16,18 @@ func main() {
cfg := loadConfig()
- git := sv.NewGit(cfg.BreakingChangePrefixes, cfg.IssueIDPrefixes, cfg.TagPattern)
+ // TODO: config using yaml
+ commitMessageCfg := sv.CommitMessageConfig{
+ Types: cfg.CommitMessageTypes,
+ Scope: sv.ScopeConfig{},
+ Footer: map[string]sv.FooterMetadataConfig{
+ "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.NewCommitMessageParser(commitMessageCfg), cfg.TagPattern)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
outputFormatter := sv.NewOutputFormatter()
diff --git a/sv/conventional_commit.go b/sv/conventional_commit.go
new file mode 100644
index 0000000..43439a1
--- /dev/null
+++ b/sv/conventional_commit.go
@@ -0,0 +1,121 @@
+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]
+}
+
+// CommitMessageParser parse commit messages.
+type CommitMessageParser interface {
+ Parse(subject, body string) CommitMessage
+}
+
+// CommitMessageParserImpl commit message parser implementation
+type CommitMessageParserImpl struct {
+ cfg CommitMessageConfig
+}
+
+// NewCommitMessageParser CommitMessageParserImpl constructor
+func NewCommitMessageParser(cfg CommitMessageConfig) CommitMessageParser {
+ return &CommitMessageParserImpl{cfg: cfg}
+}
+
+// Parse parse a commit message
+func (p CommitMessageParserImpl) 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
new file mode 100644
index 0000000..ac11881
--- /dev/null
+++ b/sv/conventional_commit_test.go
@@ -0,0 +1,60 @@
+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 TestCommitMessageParserImpl_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 := NewCommitMessageParser(cfg)
+ if got := p.Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("CommitMessageParserImpl.Parse() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/sv/formatter.go b/sv/formatter.go
index c0d7e05..4ce87ec 100644
--- a/sv/formatter.go
+++ b/sv/formatter.go
@@ -22,7 +22,7 @@ const (
{{- end}}
`
- rnSectionItem = "- {{if .Scope}}**{{.Scope}}:** {{end}}{{.Subject}} ({{.Hash}}){{if .Metadata.issueid}} ({{.Metadata.issueid}}){{end}}"
+ rnSectionItem = "- {{if .Message.Scope}}**{{.Message.Scope}}:** {{end}}{{.Message.Description}} ({{.Hash}}){{if .Message.Metadata.issue}} ({{.Message.Metadata.issue}}){{end}}"
rnSection = `{{- if .}}
diff --git a/sv/git.go b/sv/git.go
index 0fceca2..41794ea 100644
--- a/sv/git.go
+++ b/sv/git.go
@@ -6,7 +6,6 @@ import (
"fmt"
"os"
"os/exec"
- "regexp"
"strings"
"time"
@@ -16,11 +15,6 @@ import (
const (
logSeparator = "##"
endLine = "~~"
-
- // BreakingChangesKey key to breaking change metadata
- BreakingChangesKey = "breakingchange"
- // IssueIDKey key to issue id metadata
- IssueIDKey = "issueid"
)
// Git commands
@@ -35,13 +29,9 @@ type Git interface {
// GitCommitLog description of a single commit log
type GitCommitLog struct {
- Date string `json:"date,omitempty"`
- Hash string `json:"hash,omitempty"`
- Type string `json:"type,omitempty"`
- Scope string `json:"scope,omitempty"`
- Subject string `json:"subject,omitempty"`
- Body string `json:"body,omitempty"`
- Metadata map[string]string `json:"metadata,omitempty"`
+ Date string `json:"date,omitempty"`
+ Hash string `json:"hash,omitempty"`
+ Message CommitMessage `json:"message,omitempty"`
}
// GitTag git tag info
@@ -74,15 +64,15 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
// GitImpl git command implementation
type GitImpl struct {
- messageMetadata map[string][]string
- tagPattern string
+ messageParser CommitMessageParser
+ tagPattern string
}
// NewGit constructor
-func NewGit(breakinChangePrefixes, issueIDPrefixes []string, tagPattern string) *GitImpl {
+func NewGit(messageParser CommitMessageParser, tagPattern string) *GitImpl {
return &GitImpl{
- messageMetadata: map[string][]string{BreakingChangesKey: breakinChangePrefixes, IssueIDKey: issueIDPrefixes},
- tagPattern: tagPattern,
+ messageParser: messageParser,
+ tagPattern: tagPattern,
}
}
@@ -119,7 +109,7 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
if err != nil {
return nil, combinedOutputErr(err, out)
}
- return parseLogOutput(g.messageMetadata, string(out)), nil
+ return parseLogOutput(g.messageParser, string(out)), nil
}
// Commit runs git commit
@@ -177,61 +167,28 @@ func parseTagsOutput(input string) ([]GitTag, error) {
return result, nil
}
-func parseLogOutput(messageMetadata map[string][]string, log string) []GitCommitLog {
+func parseLogOutput(messageParser CommitMessageParser, log string) []GitCommitLog {
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(messageMetadata, text))
+ logs = append(logs, parseCommitLog(messageParser, text))
}
}
return logs
}
-func parseCommitLog(messageMetadata map[string][]string, commit string) GitCommitLog {
+func parseCommitLog(messageParser CommitMessageParser, commit string) GitCommitLog {
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
- commitType, scope, subject := parseCommitLogMessage(content[2])
-
- metadata := make(map[string]string)
- for key, prefixes := range messageMetadata {
- for _, prefix := range prefixes {
- if tagValue := extractTag(prefix, content[3]); tagValue != "" {
- metadata[key] = tagValue
- break
- }
- }
- }
return GitCommitLog{
- Date: content[0],
- Hash: content[1],
- Type: commitType,
- Scope: scope,
- Subject: subject,
- Body: content[3],
- Metadata: metadata,
+ Date: content[0],
+ Hash: content[1],
+ Message: messageParser.Parse(content[2], content[3]),
}
}
-func parseCommitLogMessage(message string) (string, string, string) {
- regex := regexp.MustCompile("([a-z]+)(\\((.*)\\))?: (.*)")
- result := regex.FindStringSubmatch(message)
- if len(result) != 5 {
- return "", "", message
- }
- return result[1], result[3], strings.TrimSpace(result[4])
-}
-
-func extractTag(tag, text string) string {
- regex := regexp.MustCompile(tag + " (.*)")
- result := regex.FindStringSubmatch(text)
- if len(result) < 2 {
- return ""
- }
- return result[1]
-}
-
func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
dataLen := len(data)
diff --git a/sv/helpers_test.go b/sv/helpers_test.go
index f2a3b61..1e2735a 100644
--- a/sv/helpers_test.go
+++ b/sv/helpers_test.go
@@ -12,10 +12,17 @@ func version(v string) semver.Version {
}
func commitlog(t string, metadata map[string]string) GitCommitLog {
+ breaking := false
+ if _, found := metadata[breakingKey]; found {
+ breaking = true
+ }
return GitCommitLog{
- Type: t,
- Subject: "subject text",
- Metadata: metadata,
+ Message: CommitMessage{
+ Type: t,
+ Description: "subject text",
+ IsBreakingChange: breaking,
+ Metadata: metadata,
+ },
}
}
diff --git a/sv/releasenotes.go b/sv/releasenotes.go
index f75194e..1ea13d9 100644
--- a/sv/releasenotes.go
+++ b/sv/releasenotes.go
@@ -26,16 +26,17 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, date time.Time
sections := make(map[string]ReleaseNoteSection)
var breakingChanges []string
for _, commit := range commits {
- if name, exists := p.tags[commit.Type]; exists {
- section, sexists := sections[commit.Type]
+ if name, exists := p.tags[commit.Message.Type]; exists {
+ section, sexists := sections[commit.Message.Type]
if !sexists {
section = ReleaseNoteSection{Name: name}
}
section.Items = append(section.Items, commit)
- sections[commit.Type] = section
+ sections[commit.Message.Type] = section
}
- if value, exists := commit.Metadata[BreakingChangesKey]; exists {
- breakingChanges = append(breakingChanges, value)
+ if commit.Message.BreakingMessage() != "" {
+ // TODO: if no message found, should use description instead?
+ breakingChanges = append(breakingChanges, commit.Message.BreakingMessage())
}
}
diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go
index f47fff7..6aba9a8 100644
--- a/sv/releasenotes_test.go
+++ b/sv/releasenotes_test.go
@@ -36,7 +36,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
name: "breaking changes tag",
version: semver.MustParse("1.0.0"),
date: date,
- commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breakingchange": "breaks"})},
+ commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breaking-change": "breaks"})},
want: releaseNote(semver.MustParse("1.0.0"), date, map[string]ReleaseNoteSection{"t1": newReleaseNoteSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}),
},
}
diff --git a/sv/semver.go b/sv/semver.go
index faa840c..132b5b3 100644
--- a/sv/semver.go
+++ b/sv/semver.go
@@ -69,16 +69,16 @@ func (p SemVerCommitsProcessorImpl) NextVersion(version semver.Version, commits
}
func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType {
- if _, exists := commit.Metadata[BreakingChangesKey]; exists {
+ if commit.Message.IsBreakingChange {
return major
}
- if _, exists := p.MajorVersionTypes[commit.Type]; exists {
+ if _, exists := p.MajorVersionTypes[commit.Message.Type]; exists {
return major
}
- if _, exists := p.MinorVersionTypes[commit.Type]; exists {
+ if _, exists := p.MinorVersionTypes[commit.Message.Type]; exists {
return minor
}
- if _, exists := p.PatchVersionTypes[commit.Type]; exists {
+ if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
return patch
}
if p.IncludeUnknownTypeAsPatch {
diff --git a/sv/semver_test.go b/sv/semver_test.go
index 1f18479..f3ea960 100644
--- a/sv/semver_test.go
+++ b/sv/semver_test.go
@@ -21,7 +21,7 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1")},
{"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0")},
{"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0")},
- {"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("patch", map[string]string{"breakingchange": "break"})}, version("1.0.0")},
+ {"breaking change update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("patch", map[string]string{"breaking-change": "break"})}, version("1.0.0")},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
From 740f05b84ae61e0247e6512998a54bb905693e32 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sat, 13 Feb 2021 23:49:24 -0300
Subject: [PATCH 02/24] refactor: rename parser to processor
---
cmd/git-sv/main.go | 2 +-
sv/conventional_commit.go | 16 ++++++++--------
sv/conventional_commit_test.go | 6 +++---
sv/git.go | 20 ++++++++++----------
4 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index b1ad572..4b93359 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -27,7 +27,7 @@ func main() {
}
////
- git := sv.NewGit(sv.NewCommitMessageParser(commitMessageCfg), cfg.TagPattern)
+ git := sv.NewGit(sv.NewCommitMessageProcessor(commitMessageCfg), cfg.TagPattern)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
outputFormatter := sv.NewOutputFormatter()
diff --git a/sv/conventional_commit.go b/sv/conventional_commit.go
index 43439a1..f15e6a8 100644
--- a/sv/conventional_commit.go
+++ b/sv/conventional_commit.go
@@ -52,23 +52,23 @@ func (m CommitMessage) BreakingMessage() string {
return m.Metadata[breakingKey]
}
-// CommitMessageParser parse commit messages.
-type CommitMessageParser interface {
+// CommitMessageProcessor parse commit messages.
+type CommitMessageProcessor interface {
Parse(subject, body string) CommitMessage
}
-// CommitMessageParserImpl commit message parser implementation
-type CommitMessageParserImpl struct {
+// CommitMessageProcessorImpl commit message processor implementation
+type CommitMessageProcessorImpl struct {
cfg CommitMessageConfig
}
-// NewCommitMessageParser CommitMessageParserImpl constructor
-func NewCommitMessageParser(cfg CommitMessageConfig) CommitMessageParser {
- return &CommitMessageParserImpl{cfg: cfg}
+// NewCommitMessageProcessor CommitMessageProcessorImpl constructor
+func NewCommitMessageProcessor(cfg CommitMessageConfig) CommitMessageProcessor {
+ return &CommitMessageProcessorImpl{cfg: cfg}
}
// Parse parse a commit message
-func (p CommitMessageParserImpl) Parse(subject, body string) CommitMessage {
+func (p CommitMessageProcessorImpl) Parse(subject, body string) CommitMessage {
commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject)
metadata := make(map[string]string)
diff --git a/sv/conventional_commit_test.go b/sv/conventional_commit_test.go
index ac11881..da87580 100644
--- a/sv/conventional_commit_test.go
+++ b/sv/conventional_commit_test.go
@@ -33,7 +33,7 @@ var hashMetadataBody = `some descriptions
Jira: JIRA-999
Refs #123`
-func TestCommitMessageParserImpl_Parse(t *testing.T) {
+func TestCommitMessageProcessorImpl_Parse(t *testing.T) {
tests := []struct {
name string
subject string
@@ -51,9 +51,9 @@ func TestCommitMessageParserImpl_Parse(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewCommitMessageParser(cfg)
+ p := NewCommitMessageProcessor(cfg)
if got := p.Parse(tt.subject, tt.body); !reflect.DeepEqual(got, tt.want) {
- t.Errorf("CommitMessageParserImpl.Parse() = %v, want %v", got, tt.want)
+ t.Errorf("CommitMessageProcessorImpl.Parse() = %v, want %v", got, tt.want)
}
})
}
diff --git a/sv/git.go b/sv/git.go
index 41794ea..bffcbe9 100644
--- a/sv/git.go
+++ b/sv/git.go
@@ -64,15 +64,15 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
// GitImpl git command implementation
type GitImpl struct {
- messageParser CommitMessageParser
- tagPattern string
+ messageProcessor CommitMessageProcessor
+ tagPattern string
}
// NewGit constructor
-func NewGit(messageParser CommitMessageParser, tagPattern string) *GitImpl {
+func NewGit(messageProcessor CommitMessageProcessor, tagPattern string) *GitImpl {
return &GitImpl{
- messageParser: messageParser,
- tagPattern: tagPattern,
+ messageProcessor: messageProcessor,
+ tagPattern: tagPattern,
}
}
@@ -109,7 +109,7 @@ func (g GitImpl) Log(lr LogRange) ([]GitCommitLog, error) {
if err != nil {
return nil, combinedOutputErr(err, out)
}
- return parseLogOutput(g.messageParser, string(out)), nil
+ return parseLogOutput(g.messageProcessor, string(out)), nil
}
// Commit runs git commit
@@ -167,25 +167,25 @@ func parseTagsOutput(input string) ([]GitTag, error) {
return result, nil
}
-func parseLogOutput(messageParser CommitMessageParser, log string) []GitCommitLog {
+func parseLogOutput(messageProcessor CommitMessageProcessor, log string) []GitCommitLog {
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(messageParser, text))
+ logs = append(logs, parseCommitLog(messageProcessor, text))
}
}
return logs
}
-func parseCommitLog(messageParser CommitMessageParser, commit string) GitCommitLog {
+func parseCommitLog(messageProcessor CommitMessageProcessor, commit string) GitCommitLog {
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
return GitCommitLog{
Date: content[0],
Hash: content[1],
- Message: messageParser.Parse(content[2], content[3]),
+ Message: messageProcessor.Parse(content[2], content[3]),
}
}
From de23ff963844089b9e095b85b47f46d17e8b4569 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 01:04:32 -0300
Subject: [PATCH 03/24] 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)
+ }
+ })
+ }
+}
From 0e7438b3a9f8bdd16543632b8bd9ec1745786667 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 01:07:07 -0300
Subject: [PATCH 04/24] style: add dot om message godocs
---
sv/message.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/sv/message.go b/sv/message.go
index 2588f5d..b34af01 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -111,7 +111,7 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
return footer, nil
}
-// IssueID try to extract issue id from branch, return empty if not found
+// IssueID try to extract issue id from branch, return empty if not found.
func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
r, err := regexp.Compile(p.branchIssueRegex)
if err != nil {
@@ -125,7 +125,7 @@ func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
return groups[2], nil
}
-// Format a commit message returning header, body and footer
+// Format a commit message returning header, body and footer.
func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string) {
var header strings.Builder
header.WriteString(msg.Type)
@@ -149,7 +149,7 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
return header.String(), msg.Body, footer.String()
}
-// Parse a commit message
+// Parse a commit message.
func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage {
commitType, scope, description, hasBreakingChange := parseSubjectMessage(subject)
From f6debee45ee34e4a58aff86030e6856dd050713a Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 01:48:11 -0300
Subject: [PATCH 05/24] refactor: create branches config
BREAKING CHANGE: remove BRANCH_ISSUE_REGEX varenv
---
cmd/git-sv/config.go | 3 ++-
cmd/git-sv/main.go | 11 +++++++++--
sv/config.go | 15 ++++++++++++++-
sv/message.go | 35 +++++++++++++++++------------------
sv/message_test.go | 25 ++++++++++++++-----------
5 files changed, 56 insertions(+), 33 deletions(-)
diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go
index 3f8d454..4e4ae97 100644
--- a/cmd/git-sv/config.go
+++ b/cmd/git-sv/config.go
@@ -20,7 +20,8 @@ type Config struct {
CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"`
IssueKeyName string `envconfig:"ISSUE_KEY_NAME" default:"jira"`
IssueRegex string `envconfig:"ISSUE_REGEX" default:"[A-Z]+-[0-9]+"`
- BranchIssueRegex string `envconfig:"BRANCH_ISSUE_REGEX" default:"^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"` //TODO breaking change: use issue regex instead of duplicating issue regex
+ BranchIssuePrefixRegex string `envconfig:"BRANCH_ISSUE_PREFIX_REGEX" default:"([a-z]+\\/)?"`
+ BranchIssueSuffixRegex string `envconfig:"BRANCH_ISSUE_SUFFIX_REGEX" default:"(-.*)?"`
}
func loadConfig() Config {
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index 1102e1c..5819bfa 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -21,13 +21,20 @@ func main() {
Types: cfg.CommitMessageTypes,
Scope: sv.CommitMessageScopeConfig{},
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:]},
"breaking-change": {Key: cfg.BreakingChangePrefixes[0], KeySynonyms: cfg.BreakingChangePrefixes[1:]},
},
+ Issue: sv.CommitMessageIssueConfig{Regex: cfg.IssueRegex},
+ }
+ branchesConfig := sv.BranchesConfig{
+ Skip: cfg.ValidateMessageSkipBranches,
+ ExpectIssue: true,
+ PrefixRegex: cfg.BranchIssuePrefixRegex,
+ SuffixRegex: cfg.BranchIssueSuffixRegex,
}
////
- messageProcessor := sv.NewMessageProcessor(commitMessageCfg, cfg.ValidateMessageSkipBranches, cfg.BranchIssueRegex)
+ messageProcessor := sv.NewMessageProcessor(commitMessageCfg, branchesConfig)
git := sv.NewGit(messageProcessor, cfg.TagPattern)
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
diff --git a/sv/config.go b/sv/config.go
index 5074461..9f3fb03 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -5,6 +5,7 @@ type CommitMessageConfig struct {
Types []string
Scope CommitMessageScopeConfig
Footer map[string]CommitMessageFooterConfig
+ Issue CommitMessageIssueConfig
}
// IssueConfig config for issue.
@@ -33,6 +34,18 @@ type CommitMessageScopeConfig struct {
type CommitMessageFooterConfig struct {
Key string
KeySynonyms []string
- Regex string
UseHash bool
}
+
+// CommitMessageIssueConfig issue preferences.
+type CommitMessageIssueConfig struct {
+ Regex string
+}
+
+// BranchesConfig branches preferences.
+type BranchesConfig struct {
+ PrefixRegex string
+ SuffixRegex string
+ ExpectIssue bool
+ Skip []string
+}
diff --git a/sv/message.go b/sv/message.go
index b34af01..0d74508 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -56,41 +56,39 @@ type MessageProcessor interface {
}
// NewMessageProcessor MessageProcessorImpl constructor
-func NewMessageProcessor(cfg CommitMessageConfig, skipBranches []string, branchIssueRegex string) *MessageProcessorImpl {
+func NewMessageProcessor(mcfg CommitMessageConfig, bcfg BranchesConfig) *MessageProcessorImpl {
return &MessageProcessorImpl{
- cfg: cfg,
- skipBranches: skipBranches,
- branchIssueRegex: branchIssueRegex,
+ messageCfg: mcfg,
+ branchesCfg: bcfg,
}
}
// MessageProcessorImpl process validate message hook.
type MessageProcessorImpl struct {
- cfg CommitMessageConfig
- skipBranches []string
- branchIssueRegex string
+ messageCfg CommitMessageConfig
+ branchesCfg BranchesConfig
}
// SkipBranch check if branch should be ignored.
func (p MessageProcessorImpl) SkipBranch(branch string) bool {
- return contains(branch, p.skipBranches)
+ return contains(branch, p.branchesCfg.Skip)
}
// Validate commit message.
func (p MessageProcessorImpl) Validate(message string) error {
- valid, err := regexp.MatchString("^("+strings.Join(p.cfg.Types, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
+ valid, err := regexp.MatchString("^("+strings.Join(p.messageCfg.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.cfg.Types)
+ return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.messageCfg.Types)
}
return nil
}
// Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
- if p.branchIssueRegex == "" || p.cfg.IssueConfig().Key == "" || hasIssueID(message, p.cfg.IssueConfig().Key) {
+ if !p.branchesCfg.ExpectIssue || p.messageCfg.IssueConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueConfig().Key) {
return "", nil //enhance disabled
}
@@ -102,9 +100,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.cfg.IssueConfig().Key, issue)
+ footer := fmt.Sprintf("%s: %s", p.messageCfg.IssueConfig().Key, issue)
- if !hasFooter(message, p.cfg.Footer[breakingKey].Key) {
+ if !hasFooter(message, p.messageCfg.Footer[breakingKey].Key) {
return "\n" + footer, nil
}
@@ -113,9 +111,10 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
// IssueID try to extract issue id from branch, return empty if not found.
func (p MessageProcessorImpl) IssueID(branch string) (string, error) {
- r, err := regexp.Compile(p.branchIssueRegex)
+ rstr := fmt.Sprintf("^%s(%s)%s$", p.branchesCfg.PrefixRegex, p.messageCfg.Issue.Regex, p.branchesCfg.SuffixRegex)
+ r, err := regexp.Compile(rstr)
if err != nil {
- return "", fmt.Errorf("could not compile issue regex: %s, error: %v", p.branchIssueRegex, err.Error())
+ return "", fmt.Errorf("could not compile issue regex: %s, error: %v", rstr, err.Error())
}
groups := r.FindStringSubmatch(branch)
@@ -137,13 +136,13 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
var footer strings.Builder
if msg.BreakingMessage() != "" {
- footer.WriteString(fmt.Sprintf("%s: %s", p.cfg.BreakingChangeConfig().Key, msg.BreakingMessage()))
+ footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.BreakingChangeConfig().Key, msg.BreakingMessage()))
}
if issue, exists := msg.Metadata[issueKey]; exists {
if footer.Len() > 0 {
footer.WriteString("\n")
}
- footer.WriteString(fmt.Sprintf("%s: %s", p.cfg.IssueConfig().Key, issue))
+ footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.IssueConfig().Key, issue))
}
return header.String(), msg.Body, footer.String()
@@ -154,7 +153,7 @@ 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 {
+ for key, mdCfg := range p.messageCfg.Footer {
prefixes := append([]string{mdCfg.Key}, mdCfg.KeySynonyms...)
for _, prefix := range prefixes {
if tagValue := extractFooterMetadata(prefix, body, mdCfg.UseHash); tagValue != "" {
diff --git a/sv/message_test.go b/sv/message_test.go
index 32e01ad..6491f67 100644
--- a/sv/message_test.go
+++ b/sv/message_test.go
@@ -5,20 +5,23 @@ import (
"testing"
)
-var cfg = CommitMessageConfig{
+var ccfg = CommitMessageConfig{
Types: []string{"feat", "fix"},
Scope: CommitMessageScopeConfig{},
Footer: map[string]CommitMessageFooterConfig{
- "issue": {Key: "jira", KeySynonyms: []string{"Jira"}, Regex: "[A-Z]+-[0-9]+"},
+ "issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
"breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
"refs": {Key: "Refs", UseHash: true},
},
+ Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}
-const (
- branchIssueRegex = "^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)?"
- issueRegex = "[A-Z]+-[0-9]+"
-)
+var bcfg = BranchesConfig{
+ ExpectIssue: true,
+ PrefixRegex: "([a-z]+\\/)?",
+ SuffixRegex: "(-.*)?",
+ Skip: []string{"develop", "master"},
+}
// messages samples start
var fullMessage = `fix: correct minor typos in code
@@ -57,7 +60,7 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
// multiline samples end
func TestMessageProcessorImpl_Validate(t *testing.T) {
- p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
+ p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
@@ -90,7 +93,7 @@ func TestMessageProcessorImpl_Validate(t *testing.T) {
}
func TestMessageProcessorImpl_Enhance(t *testing.T) {
- p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
+ p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
@@ -123,7 +126,7 @@ func TestMessageProcessorImpl_Enhance(t *testing.T) {
}
func TestMessageProcessorImpl_IssueID(t *testing.T) {
- p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
+ p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
@@ -251,7 +254,7 @@ Jira: JIRA-999
Refs #123`
func TestMessageProcessorImpl_Parse(t *testing.T) {
- p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
+ p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
@@ -278,7 +281,7 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
}
func TestMessageProcessorImpl_Format(t *testing.T) {
- p := NewMessageProcessor(cfg, []string{"develop", "master"}, branchIssueRegex)
+ p := NewMessageProcessor(ccfg, bcfg)
tests := []struct {
name string
From 9b63aacd8dfd2e14112bc687618ad54adcb7a6fb Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 02:32:23 -0300
Subject: [PATCH 06/24] refactor: add versioning, tag and release notes config
---
cmd/git-sv/main.go | 14 +++++++++++---
sv/config.go | 28 ++++++++++++++++++++++++++++
sv/git.go | 8 ++++----
sv/releasenotes.go | 8 ++++----
sv/releasenotes_test.go | 2 +-
sv/semver.go | 10 +++++-----
sv/semver_test.go | 2 +-
7 files changed, 54 insertions(+), 18 deletions(-)
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index 5819bfa..6f05d80 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -32,12 +32,20 @@ func main() {
PrefixRegex: cfg.BranchIssuePrefixRegex,
SuffixRegex: cfg.BranchIssueSuffixRegex,
}
+ versioningConfig := sv.VersioningConfig{
+ UpdateMajor: cfg.MajorVersionTypes,
+ UpdateMinor: cfg.MinorVersionTypes,
+ UpdatePatch: cfg.PatchVersionTypes,
+ UnknownTypeAsPatch: cfg.IncludeUnknownTypeAsPatch,
+ }
+ tagConfig := sv.TagConfig{Pattern: cfg.TagPattern}
+ releaseNotesConfig := sv.ReleaseNotesConfig{Headers: cfg.ReleaseNotesTags}
////
messageProcessor := sv.NewMessageProcessor(commitMessageCfg, branchesConfig)
- git := sv.NewGit(messageProcessor, cfg.TagPattern)
- semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
- releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
+ git := sv.NewGit(messageProcessor, tagConfig)
+ semverProcessor := sv.NewSemVerCommitsProcessor(versioningConfig)
+ releasenotesProcessor := sv.NewReleaseNoteProcessor(releaseNotesConfig)
outputFormatter := sv.NewOutputFormatter()
app := cli.NewApp()
diff --git a/sv/config.go b/sv/config.go
index 9f3fb03..74e8c79 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -1,5 +1,7 @@
package sv
+// ==== Message ====
+
// CommitMessageConfig config a commit message.
type CommitMessageConfig struct {
Types []string
@@ -42,6 +44,8 @@ type CommitMessageIssueConfig struct {
Regex string
}
+// ==== Branches ====
+
// BranchesConfig branches preferences.
type BranchesConfig struct {
PrefixRegex string
@@ -49,3 +53,27 @@ type BranchesConfig struct {
ExpectIssue bool
Skip []string
}
+
+// ==== Versioning ====
+
+// VersioningConfig versioning preferences.
+type VersioningConfig struct {
+ UpdateMajor []string
+ UpdateMinor []string
+ UpdatePatch []string
+ UnknownTypeAsPatch bool
+}
+
+// ==== Tag ====
+
+// TagConfig tag preferences.
+type TagConfig struct {
+ Pattern string
+}
+
+// ==== Release Notes ====
+
+// ReleaseNotesConfig release notes preferences.
+type ReleaseNotesConfig struct {
+ Headers map[string]string
+}
diff --git a/sv/git.go b/sv/git.go
index 3a75c94..0a130c7 100644
--- a/sv/git.go
+++ b/sv/git.go
@@ -65,14 +65,14 @@ func NewLogRange(t LogRangeType, start, end string) LogRange {
// GitImpl git command implementation
type GitImpl struct {
messageProcessor MessageProcessor
- tagPattern string
+ tagCfg TagConfig
}
// NewGit constructor
-func NewGit(messageProcessor MessageProcessor, tagPattern string) *GitImpl {
+func NewGit(messageProcessor MessageProcessor, cfg TagConfig) *GitImpl {
return &GitImpl{
messageProcessor: messageProcessor,
- tagPattern: tagPattern,
+ tagCfg: cfg,
}
}
@@ -122,7 +122,7 @@ func (g GitImpl) Commit(header, body, footer string) error {
// Tag create a git tag
func (g GitImpl) Tag(version semver.Version) error {
- tag := fmt.Sprintf(g.tagPattern, version.Major(), version.Minor(), version.Patch())
+ tag := fmt.Sprintf(g.tagCfg.Pattern, version.Major(), version.Minor(), version.Patch())
tagMsg := fmt.Sprintf("Version %d.%d.%d", version.Major(), version.Minor(), version.Patch())
tagCommand := exec.Command("git", "tag", "-a", tag, "-m", tagMsg)
diff --git a/sv/releasenotes.go b/sv/releasenotes.go
index 1ea13d9..86b196d 100644
--- a/sv/releasenotes.go
+++ b/sv/releasenotes.go
@@ -13,12 +13,12 @@ type ReleaseNoteProcessor interface {
// ReleaseNoteProcessorImpl release note based on commit log.
type ReleaseNoteProcessorImpl struct {
- tags map[string]string
+ cfg ReleaseNotesConfig
}
// NewReleaseNoteProcessor ReleaseNoteProcessor constructor.
-func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl {
- return &ReleaseNoteProcessorImpl{tags: tags}
+func NewReleaseNoteProcessor(cfg ReleaseNotesConfig) *ReleaseNoteProcessorImpl {
+ return &ReleaseNoteProcessorImpl{cfg: cfg}
}
// Create create a release note based on commits.
@@ -26,7 +26,7 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, date time.Time
sections := make(map[string]ReleaseNoteSection)
var breakingChanges []string
for _, commit := range commits {
- if name, exists := p.tags[commit.Message.Type]; exists {
+ if name, exists := p.cfg.Headers[commit.Message.Type]; exists {
section, sexists := sections[commit.Message.Type]
if !sexists {
section = ReleaseNoteSection{Name: name}
diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go
index 6aba9a8..aa09df0 100644
--- a/sv/releasenotes_test.go
+++ b/sv/releasenotes_test.go
@@ -42,7 +42,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewReleaseNoteProcessor(map[string]string{"t1": "Tag 1", "t2": "Tag 2"})
+ p := NewReleaseNoteProcessor(ReleaseNotesConfig{Headers: map[string]string{"t1": "Tag 1", "t2": "Tag 2"}})
if got := p.Create(tt.version, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
}
diff --git a/sv/semver.go b/sv/semver.go
index 132b5b3..08bf072 100644
--- a/sv/semver.go
+++ b/sv/semver.go
@@ -38,12 +38,12 @@ type SemVerCommitsProcessorImpl struct {
}
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor
-func NewSemVerCommitsProcessor(unknownAsPatch bool, majorTypes, minorTypes, patchTypes []string) *SemVerCommitsProcessorImpl {
+func NewSemVerCommitsProcessor(cfg VersioningConfig) *SemVerCommitsProcessorImpl {
return &SemVerCommitsProcessorImpl{
- IncludeUnknownTypeAsPatch: unknownAsPatch,
- MajorVersionTypes: toMap(majorTypes),
- MinorVersionTypes: toMap(minorTypes),
- PatchVersionTypes: toMap(patchTypes),
+ IncludeUnknownTypeAsPatch: cfg.UnknownTypeAsPatch,
+ MajorVersionTypes: toMap(cfg.UpdateMajor),
+ MinorVersionTypes: toMap(cfg.UpdateMinor),
+ PatchVersionTypes: toMap(cfg.UpdatePatch),
}
}
diff --git a/sv/semver_test.go b/sv/semver_test.go
index f3ea960..5d3f1d6 100644
--- a/sv/semver_test.go
+++ b/sv/semver_test.go
@@ -25,7 +25,7 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewSemVerCommitsProcessor(tt.unknownAsPatch, []string{"major"}, []string{"minor"}, []string{"patch"})
+ p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, UnknownTypeAsPatch: tt.unknownAsPatch})
if got := p.NextVersion(tt.version, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want)
}
From e70283a0c5ee0163607731c00414a98b10a00056 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 19:42:22 -0300
Subject: [PATCH 07/24] feat: config sv4git using yaml
BREAKING CHANGE: stop using var envs to config sv4git
---
cmd/git-sv/config.go | 80 ++++++++++++++++++++++++++++++++++++++---
cmd/git-sv/handlers.go | 26 +++++++++++++-
cmd/git-sv/main.go | 81 ++++++++++++++++++++++++++----------------
go.mod | 2 ++
go.sum | 6 ++++
sv/config.go | 40 ++++++++++-----------
sv/message.go | 2 +-
sv/message_test.go | 1 -
sv/semver.go | 2 +-
sv/semver_test.go | 18 +++++-----
10 files changed, 190 insertions(+), 68 deletions(-)
diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go
index 4e4ae97..a006802 100644
--- a/cmd/git-sv/config.go
+++ b/cmd/git-sv/config.go
@@ -1,13 +1,22 @@
package main
import (
+ "errors"
+ "fmt"
+ "io/ioutil"
"log"
+ "os/exec"
+ "strings"
+ "sv4git/sv"
"github.com/kelseyhightower/envconfig"
+ "gopkg.in/yaml.v3"
)
-// Config env vars for cli configuration
-type Config struct {
+// EnvConfig env vars for cli configuration
+type EnvConfig struct {
+ Home string `envconfig:"SV4GIT_HOME" default:""`
+
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,chore,docs,fix,perf,refactor,style,test"`
@@ -24,11 +33,74 @@ type Config struct {
BranchIssueSuffixRegex string `envconfig:"BRANCH_ISSUE_SUFFIX_REGEX" default:"(-.*)?"`
}
-func loadConfig() Config {
- var c Config
+func loadEnvConfig() EnvConfig {
+ var c EnvConfig
err := envconfig.Process("SV4GIT", &c)
if err != nil {
log.Fatal(err.Error())
}
return c
}
+
+// Config cli yaml config
+type Config struct {
+ Version string `yaml:"version"`
+ Versioning sv.VersioningConfig `yaml:"versioning"`
+ Tag sv.TagConfig `yaml:"tag"`
+ ReleaseNotes sv.ReleaseNotesConfig `yaml:"release-notes"`
+ Branches sv.BranchesConfig `yaml:"branches"`
+ CommitMessage sv.CommitMessageConfig `yaml:"commit-message"`
+}
+
+func getRepoPath() (string, error) {
+ cmd := exec.Command("git", "rev-parse", "--show-toplevel")
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ return "", errors.New(string(out))
+ }
+ return strings.TrimSpace(string(out)), nil
+}
+
+func loadConfig(filepath string) (Config, error) {
+ content, rerr := ioutil.ReadFile(filepath)
+ if rerr != nil {
+ return Config{}, rerr
+ }
+
+ var cfg Config
+ cerr := yaml.Unmarshal(content, &cfg)
+ if cerr != nil {
+ return Config{}, fmt.Errorf("could not parse config from path: %s, error: %v", filepath, cerr)
+ }
+
+ return cfg, nil
+}
+
+func defaultConfig() Config {
+ return Config{
+ Version: "1.0",
+ Versioning: sv.VersioningConfig{
+ UpdateMajor: []string{},
+ UpdateMinor: []string{"feat"},
+ UpdatePatch: []string{"build", "ci", "chore", "docs", "fix", "perf", "refactor", "style", "test"},
+ IgnoreUnknown: false,
+ },
+ Tag: sv.TagConfig{Pattern: "%d.%d.%d"},
+ ReleaseNotes: sv.ReleaseNotesConfig{Headers: map[string]string{"fix": "Bug Fixes", "feat": "Features", "breaking-change": "Breaking Changes"}},
+ Branches: sv.BranchesConfig{
+ PrefixRegex: "([a-z]+\\/)?",
+ SuffixRegex: "(-.*)?",
+ DisableIssue: false,
+ Skip: []string{"master", "main", "developer"},
+ },
+ CommitMessage: sv.CommitMessageConfig{
+ Types: []string{"build", "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"},
+ Scope: sv.CommitMessageScopeConfig{},
+ Footer: map[string]sv.CommitMessageFooterConfig{
+ "issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}},
+ "breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
+ },
+ Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
+ },
+ }
+}
diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go
index 8bd9595..7ac3a24 100644
--- a/cmd/git-sv/handlers.go
+++ b/cmd/git-sv/handlers.go
@@ -12,8 +12,32 @@ import (
"github.com/Masterminds/semver/v3"
"github.com/urfave/cli/v2"
+ "gopkg.in/yaml.v3"
)
+func configDefaultHandler() func(c *cli.Context) error {
+ cfg := defaultConfig()
+ return func(c *cli.Context) error {
+ content, err := yaml.Marshal(&cfg)
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(content))
+ return nil
+ }
+}
+
+func configShowHandler(cfg Config) func(c *cli.Context) error {
+ return func(c *cli.Context) error {
+ content, err := yaml.Marshal(&cfg)
+ if err != nil {
+ return err
+ }
+ fmt.Println(string(content))
+ return nil
+ }
+}
+
func currentVersionHandler(git sv.Git) func(c *cli.Context) error {
return func(c *cli.Context) error {
describe := git.Describe()
@@ -238,7 +262,7 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
}
}
-func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
+func commitHandler(cfg EnvConfig, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error {
ctype, err := promptType()
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index 6f05d80..ffdb0d9 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -3,49 +3,51 @@ package main
import (
"log"
"os"
+ "path/filepath"
"sv4git/sv"
+ "github.com/imdario/mergo"
"github.com/urfave/cli/v2"
)
// Version for git-sv
var Version = ""
+const (
+ configFilename = "config.yml"
+ repoConfigFilename = ".sv4git.yml"
+)
+
func main() {
log.SetFlags(0)
- cfg := loadConfig()
+ envCfg := loadEnvConfig()
- // TODO: config using yaml
- commitMessageCfg := sv.CommitMessageConfig{
- Types: cfg.CommitMessageTypes,
- Scope: sv.CommitMessageScopeConfig{},
- Footer: map[string]sv.CommitMessageFooterConfig{
- "issue": {Key: cfg.IssueIDPrefixes[0], KeySynonyms: cfg.IssueIDPrefixes[1:]},
- "breaking-change": {Key: cfg.BreakingChangePrefixes[0], KeySynonyms: cfg.BreakingChangePrefixes[1:]},
- },
- Issue: sv.CommitMessageIssueConfig{Regex: cfg.IssueRegex},
- }
- branchesConfig := sv.BranchesConfig{
- Skip: cfg.ValidateMessageSkipBranches,
- ExpectIssue: true,
- PrefixRegex: cfg.BranchIssuePrefixRegex,
- SuffixRegex: cfg.BranchIssueSuffixRegex,
- }
- versioningConfig := sv.VersioningConfig{
- UpdateMajor: cfg.MajorVersionTypes,
- UpdateMinor: cfg.MinorVersionTypes,
- UpdatePatch: cfg.PatchVersionTypes,
- UnknownTypeAsPatch: cfg.IncludeUnknownTypeAsPatch,
- }
- tagConfig := sv.TagConfig{Pattern: cfg.TagPattern}
- releaseNotesConfig := sv.ReleaseNotesConfig{Headers: cfg.ReleaseNotesTags}
- ////
+ cfg := defaultConfig()
- messageProcessor := sv.NewMessageProcessor(commitMessageCfg, branchesConfig)
- git := sv.NewGit(messageProcessor, tagConfig)
- semverProcessor := sv.NewSemVerCommitsProcessor(versioningConfig)
- releasenotesProcessor := sv.NewReleaseNoteProcessor(releaseNotesConfig)
+ if envCfg.Home != "" {
+ if homeCfg, err := loadConfig(filepath.Join(envCfg.Home, configFilename)); err == nil {
+ if merr := mergo.Merge(&cfg, homeCfg, mergo.WithOverride); merr != nil {
+ log.Fatal(merr)
+ }
+ }
+ }
+
+ repoPath, rerr := getRepoPath()
+ if rerr != nil {
+ log.Fatal(rerr)
+ }
+
+ if repoCfg, err := loadConfig(filepath.Join(repoPath, repoConfigFilename)); err == nil {
+ if merr := mergo.Merge(&cfg, repoCfg, mergo.WithOverride); merr != nil {
+ log.Fatal(merr)
+ }
+ }
+
+ messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
+ git := sv.NewGit(messageProcessor, cfg.Tag)
+ semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning)
+ releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
outputFormatter := sv.NewOutputFormatter()
app := cli.NewApp()
@@ -53,6 +55,23 @@ func main() {
app.Version = Version
app.Usage = "semantic version for git"
app.Commands = []*cli.Command{
+ {
+ Name: "config",
+ Aliases: []string{"cfg"},
+ Usage: "cli configuration",
+ Subcommands: []*cli.Command{
+ {
+ Name: "default",
+ Usage: "show default config",
+ Action: configDefaultHandler(),
+ },
+ {
+ Name: "show",
+ Usage: "show current config",
+ Action: configShowHandler(cfg),
+ },
+ },
+ },
{
Name: "current-version",
Aliases: []string{"cv"},
@@ -117,7 +136,7 @@ func main() {
Name: "commit",
Aliases: []string{"cmt"},
Usage: "execute git commit with convetional commit message helper",
- Action: commitHandler(cfg, git, messageProcessor),
+ Action: commitHandler(envCfg, git, messageProcessor),
},
{
Name: "validate-commit-message",
diff --git a/go.mod b/go.mod
index d39381e..3f98cf5 100644
--- a/go.mod
+++ b/go.mod
@@ -5,6 +5,7 @@ go 1.15
require (
github.com/Masterminds/semver/v3 v3.1.1
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
+ github.com/imdario/mergo v0.3.11
github.com/kelseyhightower/envconfig v1.4.0
github.com/kr/text v0.2.0 // indirect
github.com/lunixbochs/vtclean v1.0.0 // indirect
@@ -14,4 +15,5 @@ require (
github.com/urfave/cli/v2 v2.3.0
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
diff --git a/go.sum b/go.sum
index b040cff..d0a91e1 100644
--- a/go.sum
+++ b/go.sum
@@ -12,6 +12,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
@@ -61,3 +63,7 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/sv/config.go b/sv/config.go
index 74e8c79..1e9e866 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -4,10 +4,10 @@ package sv
// CommitMessageConfig config a commit message.
type CommitMessageConfig struct {
- Types []string
- Scope CommitMessageScopeConfig
- Footer map[string]CommitMessageFooterConfig
- Issue CommitMessageIssueConfig
+ Types []string `yaml:"types"`
+ Scope CommitMessageScopeConfig `yaml:"scope"`
+ Footer map[string]CommitMessageFooterConfig `yaml:"footer"`
+ Issue CommitMessageIssueConfig `yaml:"issue"`
}
// IssueConfig config for issue.
@@ -28,52 +28,52 @@ func (c CommitMessageConfig) BreakingChangeConfig() CommitMessageFooterConfig {
// CommitMessageScopeConfig config scope preferences.
type CommitMessageScopeConfig struct {
- Mandatory bool
- Values []string
+ Mandatory bool `yaml:"mandatory"`
+ Values []string `yaml:"values"`
}
// CommitMessageFooterConfig config footer metadata.
type CommitMessageFooterConfig struct {
- Key string
- KeySynonyms []string
- UseHash bool
+ Key string `yaml:"key"`
+ KeySynonyms []string `yaml:"key-synonyms"`
+ UseHash bool `yaml:"use-hash"`
}
// CommitMessageIssueConfig issue preferences.
type CommitMessageIssueConfig struct {
- Regex string
+ Regex string `yaml:"regex"`
}
// ==== Branches ====
// BranchesConfig branches preferences.
type BranchesConfig struct {
- PrefixRegex string
- SuffixRegex string
- ExpectIssue bool
- Skip []string
+ PrefixRegex string `yaml:"prefix"`
+ SuffixRegex string `yaml:"sufix"`
+ DisableIssue bool `yaml:"disable-issue"`
+ Skip []string `yaml:"skip"`
}
// ==== Versioning ====
// VersioningConfig versioning preferences.
type VersioningConfig struct {
- UpdateMajor []string
- UpdateMinor []string
- UpdatePatch []string
- UnknownTypeAsPatch bool
+ UpdateMajor []string `yaml:"update-major"`
+ UpdateMinor []string `yaml:"update-minor"`
+ UpdatePatch []string `yaml:"update-patch"`
+ IgnoreUnknown bool `yaml:"ignore-unknown"`
}
// ==== Tag ====
// TagConfig tag preferences.
type TagConfig struct {
- Pattern string
+ Pattern string `yaml:"pattern"`
}
// ==== Release Notes ====
// ReleaseNotesConfig release notes preferences.
type ReleaseNotesConfig struct {
- Headers map[string]string
+ Headers map[string]string `yaml:"headers"`
}
diff --git a/sv/message.go b/sv/message.go
index 0d74508..505ac7c 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -88,7 +88,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
// Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
- if !p.branchesCfg.ExpectIssue || p.messageCfg.IssueConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueConfig().Key) {
+ if p.branchesCfg.DisableIssue || p.messageCfg.IssueConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueConfig().Key) {
return "", nil //enhance disabled
}
diff --git a/sv/message_test.go b/sv/message_test.go
index 6491f67..7de0712 100644
--- a/sv/message_test.go
+++ b/sv/message_test.go
@@ -17,7 +17,6 @@ var ccfg = CommitMessageConfig{
}
var bcfg = BranchesConfig{
- ExpectIssue: true,
PrefixRegex: "([a-z]+\\/)?",
SuffixRegex: "(-.*)?",
Skip: []string{"develop", "master"},
diff --git a/sv/semver.go b/sv/semver.go
index 08bf072..a58365d 100644
--- a/sv/semver.go
+++ b/sv/semver.go
@@ -40,7 +40,7 @@ type SemVerCommitsProcessorImpl struct {
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor
func NewSemVerCommitsProcessor(cfg VersioningConfig) *SemVerCommitsProcessorImpl {
return &SemVerCommitsProcessorImpl{
- IncludeUnknownTypeAsPatch: cfg.UnknownTypeAsPatch,
+ IncludeUnknownTypeAsPatch: !cfg.IgnoreUnknown,
MajorVersionTypes: toMap(cfg.UpdateMajor),
MinorVersionTypes: toMap(cfg.UpdateMinor),
PatchVersionTypes: toMap(cfg.UpdatePatch),
diff --git a/sv/semver_test.go b/sv/semver_test.go
index 5d3f1d6..3117e16 100644
--- a/sv/semver_test.go
+++ b/sv/semver_test.go
@@ -9,15 +9,15 @@ import (
func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
tests := []struct {
- name string
- unknownAsPatch bool
- version semver.Version
- commits []GitCommitLog
- want semver.Version
+ name string
+ ignoreUnknown bool
+ version semver.Version
+ commits []GitCommitLog
+ want semver.Version
}{
- {"no update", false, version("0.0.0"), []GitCommitLog{}, version("0.0.0")},
- {"no update on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")},
- {"update patch on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")},
+ {"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0")},
+ {"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")},
+ {"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")},
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1")},
{"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0")},
{"major update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("major", map[string]string{})}, version("1.0.0")},
@@ -25,7 +25,7 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, UnknownTypeAsPatch: tt.unknownAsPatch})
+ p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown})
if got := p.NextVersion(tt.version, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want)
}
From 3aa2ecc487ec00b0bb5847a81d3b8f6b630f2bf8 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 20:02:11 -0300
Subject: [PATCH 08/24] refactor: remove unused var envs
---
cmd/git-sv/config.go | 17 +----------------
cmd/git-sv/handlers.go | 4 ++--
cmd/git-sv/main.go | 2 +-
sv/config.go | 8 ++++----
sv/message.go | 8 ++++----
5 files changed, 12 insertions(+), 27 deletions(-)
diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go
index a006802..eac5c58 100644
--- a/cmd/git-sv/config.go
+++ b/cmd/git-sv/config.go
@@ -16,26 +16,11 @@ import (
// EnvConfig env vars for cli configuration
type EnvConfig struct {
Home string `envconfig:"SV4GIT_HOME" default:""`
-
- MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
- MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
- PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,chore,docs,fix,perf,refactor,style,test"`
- IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
- BreakingChangePrefixes []string `envconfig:"BRAKING_CHANGE_PREFIXES" default:"BREAKING CHANGE,BREAKING CHANGES"`
- IssueIDPrefixes []string `envconfig:"ISSUEID_PREFIXES" default:"jira,JIRA,Jira"`
- TagPattern string `envconfig:"TAG_PATTERN" default:"%d.%d.%d"`
- ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
- ValidateMessageSkipBranches []string `envconfig:"VALIDATE_MESSAGE_SKIP_BRANCHES" default:"master,develop"`
- CommitMessageTypes []string `envconfig:"COMMIT_MESSAGE_TYPES" default:"build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test"`
- IssueKeyName string `envconfig:"ISSUE_KEY_NAME" default:"jira"`
- IssueRegex string `envconfig:"ISSUE_REGEX" default:"[A-Z]+-[0-9]+"`
- BranchIssuePrefixRegex string `envconfig:"BRANCH_ISSUE_PREFIX_REGEX" default:"([a-z]+\\/)?"`
- BranchIssueSuffixRegex string `envconfig:"BRANCH_ISSUE_SUFFIX_REGEX" default:"(-.*)?"`
}
func loadEnvConfig() EnvConfig {
var c EnvConfig
- err := envconfig.Process("SV4GIT", &c)
+ err := envconfig.Process("", &c)
if err != nil {
log.Fatal(err.Error())
}
diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go
index 7ac3a24..07178c3 100644
--- a/cmd/git-sv/handlers.go
+++ b/cmd/git-sv/handlers.go
@@ -262,7 +262,7 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
}
}
-func commitHandler(cfg EnvConfig, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
+func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error {
ctype, err := promptType()
@@ -297,7 +297,7 @@ func commitHandler(cfg EnvConfig, git sv.Git, messageProcessor sv.MessageProcess
if err != nil {
return err
}
- issue, err := promptIssueID(cfg.IssueKeyName, cfg.IssueRegex, branchIssue)
+ issue, err := promptIssueID(cfg.CommitMessage.IssueFooterConfig().Key, cfg.CommitMessage.Issue.Regex, branchIssue)
if err != nil {
return err
}
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index ffdb0d9..c7c7c10 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -136,7 +136,7 @@ func main() {
Name: "commit",
Aliases: []string{"cmt"},
Usage: "execute git commit with convetional commit message helper",
- Action: commitHandler(envCfg, git, messageProcessor),
+ Action: commitHandler(cfg, git, messageProcessor),
},
{
Name: "validate-commit-message",
diff --git a/sv/config.go b/sv/config.go
index 1e9e866..f140052 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -10,16 +10,16 @@ type CommitMessageConfig struct {
Issue CommitMessageIssueConfig `yaml:"issue"`
}
-// IssueConfig config for issue.
-func (c CommitMessageConfig) IssueConfig() CommitMessageFooterConfig {
+// IssueFooterConfig config for issue.
+func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
if v, exists := c.Footer[issueKey]; exists {
return v
}
return CommitMessageFooterConfig{}
}
-// BreakingChangeConfig config for breaking changes.
-func (c CommitMessageConfig) BreakingChangeConfig() CommitMessageFooterConfig {
+// BreakingChangeFooterConfig config for breaking changes.
+func (c CommitMessageConfig) BreakingChangeFooterConfig() CommitMessageFooterConfig {
if v, exists := c.Footer[breakingKey]; exists {
return v
}
diff --git a/sv/message.go b/sv/message.go
index 505ac7c..fed8dfd 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -88,7 +88,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
// Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
- if p.branchesCfg.DisableIssue || p.messageCfg.IssueConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueConfig().Key) {
+ if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig().Key) {
return "", nil //enhance disabled
}
@@ -100,7 +100,7 @@ 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.messageCfg.IssueConfig().Key, issue)
+ footer := fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue)
if !hasFooter(message, p.messageCfg.Footer[breakingKey].Key) {
return "\n" + footer, nil
@@ -136,13 +136,13 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
var footer strings.Builder
if msg.BreakingMessage() != "" {
- footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.BreakingChangeConfig().Key, msg.BreakingMessage()))
+ footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.BreakingChangeFooterConfig().Key, msg.BreakingMessage()))
}
if issue, exists := msg.Metadata[issueKey]; exists {
if footer.Len() > 0 {
footer.WriteString("\n")
}
- footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.IssueConfig().Key, issue))
+ footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue))
}
return header.String(), msg.Body, footer.String()
From d88d185b2b71fb6a271c76c37f7c27f73a3abaa2 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 20:12:36 -0300
Subject: [PATCH 09/24] feat: prompt select for scope if scope.values is
defined
---
cmd/git-sv/handlers.go | 2 +-
cmd/git-sv/prompt.go | 9 ++++++++-
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go
index 07178c3..fcb6b87 100644
--- a/cmd/git-sv/handlers.go
+++ b/cmd/git-sv/handlers.go
@@ -270,7 +270,7 @@ func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor)
return err
}
- scope, err := promptScope()
+ scope, err := promptScope(cfg.CommitMessage.Scope.Values)
if err != nil {
return err
}
diff --git a/cmd/git-sv/prompt.go b/cmd/git-sv/prompt.go
index 0f935fd..9f008fd 100644
--- a/cmd/git-sv/prompt.go
+++ b/cmd/git-sv/prompt.go
@@ -46,7 +46,14 @@ func promptType() (commitType, error) {
return items[i], nil
}
-func promptScope() (string, error) {
+func promptScope(values []string) (string, error) {
+ if len(values) > 0 {
+ selected, err := promptSelect("scope", values, nil)
+ if err != nil {
+ return "", err
+ }
+ return values[selected], nil
+ }
return promptText("scope", "^[a-z0-9-]*$", "")
}
From dd5b15af22f8884f1f9548e767b6d90441ccc9a9 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 20:18:29 -0300
Subject: [PATCH 10/24] refactor: remove mandatory config from scope
---
sv/config.go | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/sv/config.go b/sv/config.go
index f140052..2670e4d 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -28,8 +28,7 @@ func (c CommitMessageConfig) BreakingChangeFooterConfig() CommitMessageFooterCon
// CommitMessageScopeConfig config scope preferences.
type CommitMessageScopeConfig struct {
- Mandatory bool `yaml:"mandatory"`
- Values []string `yaml:"values"`
+ Values []string `yaml:"values"`
}
// CommitMessageFooterConfig config footer metadata.
From e67ae6c85973e473a493dfdda65c066161f9b121 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 20:31:18 -0300
Subject: [PATCH 11/24] feat: use configured commit types on commit comand
---
cmd/git-sv/handlers.go | 2 +-
cmd/git-sv/prompt.go | 34 ++++++++++++++++++++++------------
2 files changed, 23 insertions(+), 13 deletions(-)
diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go
index fcb6b87..6f4a1f0 100644
--- a/cmd/git-sv/handlers.go
+++ b/cmd/git-sv/handlers.go
@@ -265,7 +265,7 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *c
func commitHandler(cfg Config, git sv.Git, messageProcessor sv.MessageProcessor) func(c *cli.Context) error {
return func(c *cli.Context) error {
- ctype, err := promptType()
+ ctype, err := promptType(cfg.CommitMessage.Types)
if err != nil {
return err
}
diff --git a/cmd/git-sv/prompt.go b/cmd/git-sv/prompt.go
index 9f008fd..b43166e 100644
--- a/cmd/git-sv/prompt.go
+++ b/cmd/git-sv/prompt.go
@@ -14,18 +14,28 @@ type commitType struct {
Example string
}
-func promptType() (commitType, error) {
- items := []commitType{
- {Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"},
- {Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"},
- {Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
- {Type: "docs", Description: "documentation only changes"},
- {Type: "feat", Description: "a new feature"},
- {Type: "fix", Description: "a bug fix"},
- {Type: "perf", Description: "a code change that improves performance"},
- {Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"},
- {Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"},
- {Type: "test", Description: "adding missing tests or correcting existing tests"},
+func promptType(types []string) (commitType, error) {
+ defaultTypes := map[string]commitType{
+ "build": {Type: "build", Description: "changes that affect the build system or external dependencies", Example: "gradle, maven, go mod, npm"},
+ "ci": {Type: "ci", Description: "changes to our CI configuration files and scripts", Example: "Circle, BrowserStack, SauceLabs"},
+ "chore": {Type: "chore", Description: "update something without impacting the user", Example: "gitignore"},
+ "docs": {Type: "docs", Description: "documentation only changes"},
+ "feat": {Type: "feat", Description: "a new feature"},
+ "fix": {Type: "fix", Description: "a bug fix"},
+ "perf": {Type: "perf", Description: "a code change that improves performance"},
+ "refactor": {Type: "refactor", Description: "a code change that neither fixes a bug nor adds a feature"},
+ "style": {Type: "style", Description: "changes that do not affect the meaning of the code", Example: "white-space, formatting, missing semi-colons, etc"},
+ "test": {Type: "test", Description: "adding missing tests or correcting existing tests"},
+ "revert": {Type: "revert", Description: "revert a single commit"},
+ }
+
+ var items []commitType
+ for _, t := range types {
+ if v, exists := defaultTypes[t]; exists {
+ items = append(items, v)
+ } else {
+ items = append(items, commitType{Type: t})
+ }
}
template := &promptui.SelectTemplates{
From 221d7cd8a7c36cff2661fcdfb1ccce75d270b903 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 20:36:07 -0300
Subject: [PATCH 12/24] fix: use filepath.Join to append path and file on
validate-commit-message command
---
cmd/git-sv/handlers.go | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go
index 6f4a1f0..206f194 100644
--- a/cmd/git-sv/handlers.go
+++ b/cmd/git-sv/handlers.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"os"
+ "path/filepath"
"sort"
"strings"
"sv4git/sv"
@@ -375,7 +376,7 @@ func validateCommitMessageHandler(git sv.Git, messageProcessor sv.MessageProcess
return nil
}
- filepath := fmt.Sprintf("%s/%s", c.String("path"), c.String("file"))
+ filepath := filepath.Join(c.String("path"), c.String("file"))
commitMessage, err := readFile(filepath)
if err != nil {
From df26b50096c2c1c161c9931456d5ce080acbd778 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Sun, 14 Feb 2021 23:17:04 -0300
Subject: [PATCH 13/24] feat: validate scope at validate-commit-message command
---
sv/message.go | 38 +++++++++++---
sv/message_test.go | 120 +++++++++++++++++++++++++++++++++------------
2 files changed, 119 insertions(+), 39 deletions(-)
diff --git a/sv/message.go b/sv/message.go
index fed8dfd..d0fdbe2 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -2,6 +2,7 @@ package sv
import (
"bufio"
+ "errors"
"fmt"
"regexp"
"strings"
@@ -76,13 +77,21 @@ func (p MessageProcessorImpl) SkipBranch(branch string) bool {
// Validate commit message.
func (p MessageProcessorImpl) Validate(message string) error {
- valid, err := regexp.MatchString("^("+strings.Join(p.messageCfg.Types, "|")+")(\\(.+\\))?!?: .*$", firstLine(message))
- if err != nil {
- return err
+ subject, body := splitCommitMessageContent(message)
+ msg := p.Parse(subject, body)
+
+ if !regexp.MustCompile("^[a-z+]+(\\(.+\\))?!?: .+$").MatchString(subject) {
+ return errors.New("message should be valid according with conventional commits")
}
- if !valid {
- return fmt.Errorf("message should contain type: %v, and should be valid according with conventional commits", p.messageCfg.Types)
+
+ if msg.Type == "" || !contains(msg.Type, p.messageCfg.Types) {
+ return fmt.Errorf("message type should be one of [%v]", strings.Join(p.messageCfg.Types, ", "))
}
+
+ if len(p.messageCfg.Scope.Values) > 0 && !contains(msg.Scope, p.messageCfg.Scope.Values) {
+ return fmt.Errorf("message scope should one of [%v]", strings.Join(p.messageCfg.Scope.Values, ", "))
+ }
+
return nil
}
@@ -230,6 +239,21 @@ func contains(value string, content []string) bool {
return false
}
-func firstLine(value string) string {
- return strings.Split(value, "\n")[0]
+func splitCommitMessageContent(content string) (string, string) {
+ scanner := bufio.NewScanner(strings.NewReader(content))
+
+ scanner.Scan()
+ subject := scanner.Text()
+
+ var body strings.Builder
+ first := true
+ for scanner.Scan() {
+ if !first {
+ body.WriteString("\n")
+ }
+ body.WriteString(scanner.Text())
+ first = false
+ }
+
+ return subject, body.String()
}
diff --git a/sv/message_test.go b/sv/message_test.go
index 7de0712..18da677 100644
--- a/sv/message_test.go
+++ b/sv/message_test.go
@@ -16,6 +16,17 @@ var ccfg = CommitMessageConfig{
Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}
+var ccfgWithScope = CommitMessageConfig{
+ Types: []string{"feat", "fix"},
+ Scope: CommitMessageScopeConfig{Values: []string{"", "scope"}},
+ Footer: map[string]CommitMessageFooterConfig{
+ "issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
+ "breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
+ "refs": {Key: "Refs", UseHash: true},
+ },
+ Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
+}
+
var bcfg = BranchesConfig{
PrefixRegex: "([a-z]+\\/)?",
SuffixRegex: "(-.*)?",
@@ -59,31 +70,33 @@ BREAKING CHANGE: refactor to use JavaScript features not available in Node 6.`
// multiline samples end
func TestMessageProcessorImpl_Validate(t *testing.T) {
- p := NewMessageProcessor(ccfg, bcfg)
-
tests := []struct {
name string
+ cfg CommitMessageConfig
message string
wantErr bool
}{
- {"single line valid message", "feat: add something", false},
- {"single line valid message with scope", "feat(scope): add something", false},
- {"single line invalid type message", "something: add something", true},
- {"single line invalid type message", "feat?: add something", true},
+ {"single line valid message", ccfg, "feat: add something", false},
+ {"single line valid message with scope", ccfg, "feat(scope): add something", false},
+ {"single line valid scope from list", ccfgWithScope, "feat(scope): add something", false},
+ {"single line invalid scope from list", ccfgWithScope, "feat(invalid): add something", true},
+ {"single line invalid type message", ccfg, "something: add something", true},
+ {"single line invalid type message", ccfg, "feat?: add something", true},
- {"multi line valid message", `feat: add something
+ {"multi line valid message", ccfg, `feat: add something
team: x`, false},
- {"multi line invalid message", `feat add something
+ {"multi line invalid message", ccfg, `feat add something
team: x`, true},
- {"support ! for breaking change", "feat!: add something", false},
- {"support ! with scope for breaking change", "feat(scope)!: add something", false},
+ {"support ! for breaking change", ccfg, "feat!: add something", false},
+ {"support ! with scope for breaking change", ccfg, "feat(scope)!: add something", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+ p := NewMessageProcessor(tt.cfg, bcfg)
if err := p.Validate(tt.message); (err != nil) != tt.wantErr {
t.Errorf("MessageProcessorImpl.Validate() error = %v, wantErr %v", err, tt.wantErr)
}
@@ -162,28 +175,6 @@ c`
jira: JIRA-123`
)
-func Test_firstLine(t *testing.T) {
- tests := []struct {
- name string
- value string
- want string
- }{
- {"empty string", "", ""},
-
- {"single line string", "single line", "single line"},
-
- {"multi line string", `first line
- last line`, "first line"},
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := firstLine(tt.value); got != tt.want {
- t.Errorf("firstLine() = %v, want %v", got, tt.want)
- }
- })
- }
-}
-
func Test_hasIssueID(t *testing.T) {
tests := []struct {
name string
@@ -312,3 +303,68 @@ func TestMessageProcessorImpl_Format(t *testing.T) {
})
}
}
+
+var expectedBodyFullMessage = `
+see the issue for details
+
+on typos fixed.
+
+Reviewed-by: Z
+Refs #133`
+
+func Test_splitCommitMessageContent(t *testing.T) {
+ tests := []struct {
+ name string
+ content string
+ wantSubject string
+ wantBody string
+ }{
+ {"single line commit", "feat: something", "feat: something", ""},
+ {"multi line commit", fullMessage, "fix: correct minor typos in code", expectedBodyFullMessage},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, got1 := splitCommitMessageContent(tt.content)
+ if got != tt.wantSubject {
+ t.Errorf("splitCommitMessageContent() subject got = %v, want %v", got, tt.wantSubject)
+ }
+ if got1 != tt.wantBody {
+ t.Errorf("splitCommitMessageContent() body got1 = [%v], want [%v]", got1, tt.wantBody)
+ }
+ })
+ }
+}
+
+//commitType, scope, description, hasBreakingChange
+func Test_parseSubjectMessage(t *testing.T) {
+ tests := []struct {
+ name string
+ message string
+ wantType string
+ wantScope string
+ wantDescription string
+ wantHasBreakingChange bool
+ }{
+ {"valid commit", "feat: something", "feat", "", "something", false},
+ {"valid commit with scope", "feat(scope): something", "feat", "scope", "something", false},
+ {"valid commit with breaking change", "feat(scope)!: something", "feat", "scope", "something", true},
+ {"missing description", "feat: ", "feat", "", "", false},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ctype, scope, description, hasBreakingChange := parseSubjectMessage(tt.message)
+ if ctype != tt.wantType {
+ t.Errorf("parseSubjectMessage() type got = %v, want %v", ctype, tt.wantType)
+ }
+ if scope != tt.wantScope {
+ t.Errorf("parseSubjectMessage() scope got = %v, want %v", scope, tt.wantScope)
+ }
+ if description != tt.wantDescription {
+ t.Errorf("parseSubjectMessage() description got = %v, want %v", description, tt.wantDescription)
+ }
+ if hasBreakingChange != tt.wantHasBreakingChange {
+ t.Errorf("parseSubjectMessage() hasBreakingChange got = %v, want %v", hasBreakingChange, tt.wantHasBreakingChange)
+ }
+ })
+ }
+}
From 2ae35c91af035f016088e5c2a1fccc56d84bbd1f Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 00:05:43 -0300
Subject: [PATCH 14/24] feat: remove breaking change synonyms support
BREAKING CHANGE: is not possible to set breaking change footer synonym
---
cmd/git-sv/config.go | 3 +--
sv/config.go | 10 +---------
sv/helpers_test.go | 2 +-
sv/message.go | 28 ++++++++++++++--------------
sv/message_test.go | 20 +++++++++-----------
5 files changed, 26 insertions(+), 37 deletions(-)
diff --git a/cmd/git-sv/config.go b/cmd/git-sv/config.go
index eac5c58..42f8e98 100644
--- a/cmd/git-sv/config.go
+++ b/cmd/git-sv/config.go
@@ -82,8 +82,7 @@ func defaultConfig() Config {
Types: []string{"build", "ci", "chore", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"},
Scope: sv.CommitMessageScopeConfig{},
Footer: map[string]sv.CommitMessageFooterConfig{
- "issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}},
- "breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
+ "issue": {Key: "jira", KeySynonyms: []string{"Jira", "JIRA"}},
},
Issue: sv.CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
},
diff --git a/sv/config.go b/sv/config.go
index 2670e4d..676f007 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -12,15 +12,7 @@ type CommitMessageConfig struct {
// IssueFooterConfig config for issue.
func (c CommitMessageConfig) IssueFooterConfig() CommitMessageFooterConfig {
- if v, exists := c.Footer[issueKey]; exists {
- return v
- }
- return CommitMessageFooterConfig{}
-}
-
-// BreakingChangeFooterConfig config for breaking changes.
-func (c CommitMessageConfig) BreakingChangeFooterConfig() CommitMessageFooterConfig {
- if v, exists := c.Footer[breakingKey]; exists {
+ if v, exists := c.Footer[issueMetadataKey]; exists {
return v
}
return CommitMessageFooterConfig{}
diff --git a/sv/helpers_test.go b/sv/helpers_test.go
index 1e2735a..b95c4f0 100644
--- a/sv/helpers_test.go
+++ b/sv/helpers_test.go
@@ -13,7 +13,7 @@ func version(v string) semver.Version {
func commitlog(t string, metadata map[string]string) GitCommitLog {
breaking := false
- if _, found := metadata[breakingKey]; found {
+ if _, found := metadata[breakingChangeMetadataKey]; found {
breaking = true
}
return GitCommitLog{
diff --git a/sv/message.go b/sv/message.go
index d0fdbe2..1ba1ed9 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -9,9 +9,9 @@ import (
)
const (
- breakingKey = "breaking-change"
- // IssueIDKey key to issue id metadata
- issueKey = "issue"
+ breakingChangeFooterKey = "BREAKING CHANGE"
+ breakingChangeMetadataKey = "breaking-change"
+ issueMetadataKey = "issue"
)
// CommitMessage is a message using conventional commits.
@@ -28,22 +28,22 @@ type CommitMessage struct {
func NewCommitMessage(ctype, scope, description, body, issue, breakingChanges string) CommitMessage {
metadata := make(map[string]string)
if issue != "" {
- metadata[issueKey] = issue
+ metadata[issueMetadataKey] = issue
}
if breakingChanges != "" {
- metadata[breakingKey] = breakingChanges
+ metadata[breakingChangeMetadataKey] = 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]
+ return m.Metadata[issueMetadataKey]
}
// BreakingMessage return breaking change message from metadata.
func (m CommitMessage) BreakingMessage() string {
- return m.Metadata[breakingKey]
+ return m.Metadata[breakingChangeMetadataKey]
}
// MessageProcessor interface.
@@ -111,7 +111,7 @@ func (p MessageProcessorImpl) Enhance(branch string, message string) (string, er
footer := fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue)
- if !hasFooter(message, p.messageCfg.Footer[breakingKey].Key) {
+ if !hasFooter(message) {
return "\n" + footer, nil
}
@@ -145,9 +145,9 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
var footer strings.Builder
if msg.BreakingMessage() != "" {
- footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.BreakingChangeFooterConfig().Key, msg.BreakingMessage()))
+ footer.WriteString(fmt.Sprintf("%s: %s", breakingChangeFooterKey, msg.BreakingMessage()))
}
- if issue, exists := msg.Metadata[issueKey]; exists {
+ if issue, exists := msg.Metadata[issueMetadataKey]; exists {
if footer.Len() > 0 {
footer.WriteString("\n")
}
@@ -171,8 +171,8 @@ func (p MessageProcessorImpl) Parse(subject, body string) CommitMessage {
}
}
}
-
- if _, exists := metadata[breakingKey]; exists {
+ if tagValue := extractFooterMetadata(breakingChangeFooterKey, body, false); tagValue != "" {
+ metadata[breakingChangeMetadataKey] = tagValue
hasBreakingChange = true
}
@@ -210,8 +210,8 @@ func extractFooterMetadata(key, text string, useHash bool) string {
return result[1]
}
-func hasFooter(message, breakingChangeKey string) bool {
- r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeKey + ": .*")
+func hasFooter(message string) bool {
+ r := regexp.MustCompile("^[a-zA-Z-]+: .*|^[a-zA-Z-]+ #.*|^" + breakingChangeFooterKey + ": .*")
scanner := bufio.NewScanner(strings.NewReader(message))
lines := 0
diff --git a/sv/message_test.go b/sv/message_test.go
index 18da677..68b3c7f 100644
--- a/sv/message_test.go
+++ b/sv/message_test.go
@@ -9,9 +9,8 @@ var ccfg = CommitMessageConfig{
Types: []string{"feat", "fix"},
Scope: CommitMessageScopeConfig{},
Footer: map[string]CommitMessageFooterConfig{
- "issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
- "breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
- "refs": {Key: "Refs", UseHash: true},
+ "issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
+ "refs": {Key: "Refs", UseHash: true},
},
Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}
@@ -20,9 +19,8 @@ var ccfgWithScope = CommitMessageConfig{
Types: []string{"feat", "fix"},
Scope: CommitMessageScopeConfig{Values: []string{"", "scope"}},
Footer: map[string]CommitMessageFooterConfig{
- "issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
- "breaking-change": {Key: "BREAKING CHANGE", KeySynonyms: []string{"BREAKING CHANGES"}},
- "refs": {Key: "Refs", UseHash: true},
+ "issue": {Key: "jira", KeySynonyms: []string{"Jira"}},
+ "refs": {Key: "Refs", UseHash: true},
},
Issue: CommitMessageIssueConfig{Regex: "[A-Z]+-[0-9]+"},
}
@@ -216,7 +214,7 @@ func Test_hasFooter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := hasFooter(tt.message, "BREAKING CHANGE"); got != tt.want {
+ if got := hasFooter(tt.message); got != tt.want {
t.Errorf("hasFooter() = %v, want %v", got, tt.want)
}
})
@@ -255,11 +253,11 @@ func TestMessageProcessorImpl_Parse(t *testing.T) {
{"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"}}},
+ {"jira and breaking change metadata", "feat: something new", completeBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: completeBody, IsBreakingChange: true, Metadata: map[string]string{issueMetadataKey: "JIRA-123", breakingChangeMetadataKey: "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{issueMetadataKey: "JIRA-456"}}},
+ {"jira synonyms metadata", "feat: something new", issueSynonymsBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: issueSynonymsBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "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"}}},
+ {"hash metadata", "feat: something new", hashMetadataBody, CommitMessage{Type: "feat", Scope: "", Description: "something new", Body: hashMetadataBody, IsBreakingChange: false, Metadata: map[string]string{issueMetadataKey: "JIRA-999", "refs": "#123"}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
From 253b77d061f82d3b6b1f5c27ea3f0921c27e9c40 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 01:03:57 -0300
Subject: [PATCH 15/24] refactor: overwrite release notes header at config
merge
---
cmd/git-sv/main.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index c7c7c10..a41c78c 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -42,6 +42,9 @@ func main() {
if merr := mergo.Merge(&cfg, repoCfg, mergo.WithOverride); merr != nil {
log.Fatal(merr)
}
+ if len(repoCfg.ReleaseNotes.Headers) > 0 { // mergo is merging maps, headers will be overwritten
+ cfg.ReleaseNotes.Headers = repoCfg.ReleaseNotes.Headers
+ }
}
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
From 11a847fe2203a0bb7a660c51b16b2efb8d248d02 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 01:47:20 -0300
Subject: [PATCH 16/24] feat: add support to rename or disable breaking changes
section from release notes
---
sv/formatter.go | 8 ++++----
sv/helpers_test.go | 10 +++++++---
sv/releasenotes.go | 14 ++++++++++++--
sv/releasenotes_test.go | 2 +-
4 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/sv/formatter.go b/sv/formatter.go
index 4ce87ec..45d32c9 100644
--- a/sv/formatter.go
+++ b/sv/formatter.go
@@ -10,7 +10,7 @@ type releaseNoteTemplateVariables struct {
Version string
Date string
Sections map[string]ReleaseNoteSection
- BreakingChanges []string
+ BreakingChanges BreakingChangeSection
}
const (
@@ -32,10 +32,10 @@ const (
{{- end}}
{{- end}}`
- rnSectionBreakingChanges = `{{- if .}}
+ rnSectionBreakingChanges = `{{- if ne .Name ""}}
-### Breaking Changes
-{{range $k,$v := .}}
+### {{.Name}}
+{{range $k,$v := .Messages}}
- {{$v}}
{{- end}}
{{- end}}`
diff --git a/sv/helpers_test.go b/sv/helpers_test.go
index b95c4f0..e4fa880 100644
--- a/sv/helpers_test.go
+++ b/sv/helpers_test.go
@@ -11,14 +11,14 @@ func version(v string) semver.Version {
return *r
}
-func commitlog(t string, metadata map[string]string) GitCommitLog {
+func commitlog(ctype string, metadata map[string]string) GitCommitLog {
breaking := false
if _, found := metadata[breakingChangeMetadataKey]; found {
breaking = true
}
return GitCommitLog{
Message: CommitMessage{
- Type: t,
+ Type: ctype,
Description: "subject text",
IsBreakingChange: breaking,
Metadata: metadata,
@@ -27,11 +27,15 @@ func commitlog(t string, metadata map[string]string) GitCommitLog {
}
func releaseNote(version *semver.Version, date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote {
+ var bchanges BreakingChangeSection
+ if len(breakingChanges) > 0 {
+ bchanges = BreakingChangeSection{Name: "Breaking Changes", Messages: breakingChanges}
+ }
return ReleaseNote{
Version: version,
Date: date.Truncate(time.Minute),
Sections: sections,
- BreakingChanges: breakingChanges,
+ BreakingChanges: bchanges,
}
}
diff --git a/sv/releasenotes.go b/sv/releasenotes.go
index 86b196d..e02c291 100644
--- a/sv/releasenotes.go
+++ b/sv/releasenotes.go
@@ -40,7 +40,11 @@ func (p ReleaseNoteProcessorImpl) Create(version *semver.Version, date time.Time
}
}
- return ReleaseNote{Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges}
+ var breakingChangeSection BreakingChangeSection
+ if name, exists := p.cfg.Headers[breakingChangeMetadataKey]; exists && len(breakingChanges) > 0 {
+ breakingChangeSection = BreakingChangeSection{Name: name, Messages: breakingChanges}
+ }
+ return ReleaseNote{Version: version, Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChangeSection}
}
// ReleaseNote release note.
@@ -48,7 +52,13 @@ type ReleaseNote struct {
Version *semver.Version
Date time.Time
Sections map[string]ReleaseNoteSection
- BreakingChanges []string
+ BreakingChanges BreakingChangeSection
+}
+
+// BreakingChangeSection breaking change section
+type BreakingChangeSection struct {
+ Name string
+ Messages []string
}
// ReleaseNoteSection release note section.
diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go
index aa09df0..9bf51f9 100644
--- a/sv/releasenotes_test.go
+++ b/sv/releasenotes_test.go
@@ -42,7 +42,7 @@ func TestReleaseNoteProcessorImpl_Create(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewReleaseNoteProcessor(ReleaseNotesConfig{Headers: map[string]string{"t1": "Tag 1", "t2": "Tag 2"}})
+ p := NewReleaseNoteProcessor(ReleaseNotesConfig{Headers: map[string]string{"t1": "Tag 1", "t2": "Tag 2", "breaking-change": "Breaking Changes"}})
if got := p.Create(tt.version, tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ReleaseNoteProcessorImpl.Create() = %v, want %v", got, tt.want)
}
From 1fc099481dd775123f8b8b7ff2829e348dc5b837 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 01:50:28 -0300
Subject: [PATCH 17/24] chore: add .sv4git.yml
---
.sv4git.yml | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
create mode 100644 .sv4git.yml
diff --git a/.sv4git.yml b/.sv4git.yml
new file mode 100644
index 0000000..3f38617
--- /dev/null
+++ b/.sv4git.yml
@@ -0,0 +1,21 @@
+version: "1.0"
+
+versioning:
+ update-major: []
+ update-minor:
+ - feat
+ update-patch:
+ - build
+ - ci
+ - chore
+ - fix
+ - perf
+ - refactor
+ - test
+
+commit-message:
+ footer:
+ issue:
+ key: issue
+ issue:
+ regex: '#[0-9]+'
From 5c992b682676d9cfacf48a35d3d90f66809ec3c1 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 02:41:43 -0300
Subject: [PATCH 18/24] feat: ignore known types on bumping version if not
mapped on bump config
---
cmd/git-sv/main.go | 2 +-
sv/semver.go | 14 ++++++++------
sv/semver_test.go | 3 ++-
3 files changed, 11 insertions(+), 8 deletions(-)
diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go
index a41c78c..d39f5c5 100644
--- a/cmd/git-sv/main.go
+++ b/cmd/git-sv/main.go
@@ -49,7 +49,7 @@ func main() {
messageProcessor := sv.NewMessageProcessor(cfg.CommitMessage, cfg.Branches)
git := sv.NewGit(messageProcessor, cfg.Tag)
- semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning)
+ semverProcessor := sv.NewSemVerCommitsProcessor(cfg.Versioning, cfg.CommitMessage)
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotes)
outputFormatter := sv.NewOutputFormatter()
diff --git a/sv/semver.go b/sv/semver.go
index a58365d..c3648e1 100644
--- a/sv/semver.go
+++ b/sv/semver.go
@@ -34,16 +34,18 @@ type SemVerCommitsProcessorImpl struct {
MajorVersionTypes map[string]struct{}
MinorVersionTypes map[string]struct{}
PatchVersionTypes map[string]struct{}
+ KnownTypes []string
IncludeUnknownTypeAsPatch bool
}
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor
-func NewSemVerCommitsProcessor(cfg VersioningConfig) *SemVerCommitsProcessorImpl {
+func NewSemVerCommitsProcessor(vcfg VersioningConfig, mcfg CommitMessageConfig) *SemVerCommitsProcessorImpl {
return &SemVerCommitsProcessorImpl{
- IncludeUnknownTypeAsPatch: !cfg.IgnoreUnknown,
- MajorVersionTypes: toMap(cfg.UpdateMajor),
- MinorVersionTypes: toMap(cfg.UpdateMinor),
- PatchVersionTypes: toMap(cfg.UpdatePatch),
+ IncludeUnknownTypeAsPatch: !vcfg.IgnoreUnknown,
+ MajorVersionTypes: toMap(vcfg.UpdateMajor),
+ MinorVersionTypes: toMap(vcfg.UpdateMinor),
+ PatchVersionTypes: toMap(vcfg.UpdatePatch),
+ KnownTypes: mcfg.Types,
}
}
@@ -81,7 +83,7 @@ func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) ver
if _, exists := p.PatchVersionTypes[commit.Message.Type]; exists {
return patch
}
- if p.IncludeUnknownTypeAsPatch {
+ if !contains(commit.Message.Type, p.KnownTypes) && p.IncludeUnknownTypeAsPatch {
return patch
}
return none
diff --git a/sv/semver_test.go b/sv/semver_test.go
index 3117e16..c1a2678 100644
--- a/sv/semver_test.go
+++ b/sv/semver_test.go
@@ -17,6 +17,7 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
}{
{"no update", true, version("0.0.0"), []GitCommitLog{}, version("0.0.0")},
{"no update on unknown type", true, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.0")},
+ {"no update on unmapped known type", false, version("0.0.0"), []GitCommitLog{commitlog("none", map[string]string{})}, version("0.0.0")},
{"update patch on unknown type", false, version("0.0.0"), []GitCommitLog{commitlog("a", map[string]string{})}, version("0.0.1")},
{"patch update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{})}, version("0.0.1")},
{"minor update", false, version("0.0.0"), []GitCommitLog{commitlog("patch", map[string]string{}), commitlog("minor", map[string]string{})}, version("0.1.0")},
@@ -25,7 +26,7 @@ func TestSemVerCommitsProcessorImpl_NextVersion(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown})
+ p := NewSemVerCommitsProcessor(VersioningConfig{UpdateMajor: []string{"major"}, UpdateMinor: []string{"minor"}, UpdatePatch: []string{"patch"}, IgnoreUnknown: tt.ignoreUnknown}, CommitMessageConfig{Types: []string{"major", "minor", "patch", "none"}})
if got := p.NextVersion(tt.version, tt.commits); !reflect.DeepEqual(got, tt.want) {
t.Errorf("SemVerCommitsProcessorImpl.NextVersion() = %v, want %v", got, tt.want)
}
From abeae14b9608feed279cdef58b886c567a601b43 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 02:51:52 -0300
Subject: [PATCH 19/24] refactor: fix suffix typo
---
sv/config.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/sv/config.go b/sv/config.go
index 676f007..cba5234 100644
--- a/sv/config.go
+++ b/sv/config.go
@@ -40,7 +40,7 @@ type CommitMessageIssueConfig struct {
// BranchesConfig branches preferences.
type BranchesConfig struct {
PrefixRegex string `yaml:"prefix"`
- SuffixRegex string `yaml:"sufix"`
+ SuffixRegex string `yaml:"suffix"`
DisableIssue bool `yaml:"disable-issue"`
Skip []string `yaml:"skip"`
}
From 57995c3458f51afe9549bc6b8a39924a2de30a7e Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 03:23:02 -0300
Subject: [PATCH 20/24] feat: support # as footer metadata separator
---
sv/message.go | 17 +++++++++++++----
sv/message_test.go | 24 +++++++++++++++---------
2 files changed, 28 insertions(+), 13 deletions(-)
diff --git a/sv/message.go b/sv/message.go
index 1ba1ed9..2208b06 100644
--- a/sv/message.go
+++ b/sv/message.go
@@ -97,7 +97,7 @@ func (p MessageProcessorImpl) Validate(message string) error {
// Enhance add metadata on commit message.
func (p MessageProcessorImpl) Enhance(branch string, message string) (string, error) {
- if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig().Key) {
+ if p.branchesCfg.DisableIssue || p.messageCfg.IssueFooterConfig().Key == "" || hasIssueID(message, p.messageCfg.IssueFooterConfig()) {
return "", nil //enhance disabled
}
@@ -151,7 +151,11 @@ func (p MessageProcessorImpl) Format(msg CommitMessage) (string, string, string)
if footer.Len() > 0 {
footer.WriteString("\n")
}
- footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue))
+ if p.messageCfg.IssueFooterConfig().UseHash {
+ footer.WriteString(fmt.Sprintf("%s #%s", p.messageCfg.IssueFooterConfig().Key, strings.TrimPrefix(issue, "#")))
+ } else {
+ footer.WriteString(fmt.Sprintf("%s: %s", p.messageCfg.IssueFooterConfig().Key, issue))
+ }
}
return header.String(), msg.Body, footer.String()
@@ -225,8 +229,13 @@ func hasFooter(message string) bool {
return false
}
-func hasIssueID(message, issueKeyName string) bool {
- r := regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueKeyName))
+func hasIssueID(message string, issueConfig CommitMessageFooterConfig) bool {
+ var r *regexp.Regexp
+ if issueConfig.UseHash {
+ r = regexp.MustCompile(fmt.Sprintf("(?m)^%s #.+$", issueConfig.Key))
+ } else {
+ r = regexp.MustCompile(fmt.Sprintf("(?m)^%s: .+$", issueConfig.Key))
+ }
return r.MatchString(message)
}
diff --git a/sv/message_test.go b/sv/message_test.go
index 68b3c7f..2bdeb2a 100644
--- a/sv/message_test.go
+++ b/sv/message_test.go
@@ -174,26 +174,32 @@ jira: JIRA-123`
)
func Test_hasIssueID(t *testing.T) {
+ cfgColon := CommitMessageFooterConfig{Key: "jira"}
+ cfgHash := CommitMessageFooterConfig{Key: "jira", UseHash: true}
+
tests := []struct {
- name string
- message string
- issueKeyName string
- want bool
+ name string
+ message string
+ issueCfg CommitMessageFooterConfig
+ want bool
}{
- {"single line without issue", "feat: something", "jira", false},
+ {"single line without issue", "feat: something", cfgColon, false},
{"multi line without issue", `feat: something
-yay`, "jira", false},
+yay`, cfgColon, false},
{"multi line without jira issue", `feat: something
-jira1: JIRA-123`, "jira", false},
+jira1: JIRA-123`, cfgColon, false},
{"multi line with issue", `feat: something
-jira: JIRA-123`, "jira", true},
+jira: JIRA-123`, cfgColon, true},
+ {"multi line with issue and hash", `feat: something
+
+jira #JIRA-123`, cfgHash, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
- if got := hasIssueID(tt.message, tt.issueKeyName); got != tt.want {
+ if got := hasIssueID(tt.message, tt.issueCfg); got != tt.want {
t.Errorf("hasIssueID() = %v, want %v", got, tt.want)
}
})
From 717edfbb1c606920ca0ec45224fa77691b1bfbbd Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 03:28:34 -0300
Subject: [PATCH 21/24] docs: add yaml information at config section
---
README.md | 125 ++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 107 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index 7f11866..2a539fe 100644
--- a/README.md
+++ b/README.md
@@ -6,27 +6,116 @@ Semantic version for git
### Installing
-download the latest release and add the binary on your path
+- Download the latest release and add the binary on your path
+- Optional: Set `SV4GIT_HOME` to define user configs, check [config](#config) for more information.
### Config
-you can config using the environment variables
+There are 3 config levels when using sv4git: [default](#default), [user](#user), [repository](#repository). All 3 are merged using the follow priority: **repository > user > default**.
-| Variable | description | default |
-| ------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
-| SV4GIT_MAJOR_VERSION_TYPES | types used to bump major version | |
-| SV4GIT_MINOR_VERSION_TYPES | types used to bump minor version | feat |
-| SV4GIT_PATCH_VERSION_TYPES | types used to bump patch version | build,ci,chore,docs,fix,perf,refactor,style,test |
-| SV4GIT_INCLUDE_UNKNOWN_TYPE_AS_PATCH | force patch bump on unknown type | true |
-| SV4GIT_BRAKING_CHANGE_PREFIXES | list of prefixes that will be used to identify a breaking change | BREAKING CHANGE:,BREAKING CHANGES: |
-| SV4GIT_ISSUEID_PREFIXES | list of prefixes that will be used to identify an issue id | jira:,JIRA:,Jira: |
-| SV4GIT_TAG_PATTERN | tag version pattern | %d.%d.%d |
-| SV4GIT_RELEASE_NOTES_TAGS | release notes headers for each visible type | fix:Bug Fixes,feat:Features |
-| SV4GIT_VALIDATE_MESSAGE_SKIP_BRANCHES | ignore branches from this list on validate commit message | master,develop |
-| SV4GIT_COMMIT_MESSAGE_TYPES | list of valid commit types for commit message | build,ci,chore,docs,feat,fix,perf,refactor,revert,style,test |
-| SV4GIT_ISSUE_KEY_NAME | metadata key name used on validate commit message hook to enhance footer, if blank footer will not be added | jira |
-| SV4GIT_ISSUE_REGEX | issue id regex, if blank footer will not be added | [A-Z]+-[0-9]+ |
-| SV4GIT_BRANCH_ISSUE_REGEX | regex to extract issue id from branch name, must have 3 groups (prefix, id, posfix), if blank footer will not be added | ^([a-z]+\\/)?([A-Z]+-[0-9]+)(-.*)? |
+To see current config, run:
+
+```bash
+git sv cfg show
+```
+
+#### Configuration types
+
+##### Default
+
+To check what is the default configuration, run:
+
+```bash
+git sv cfg default
+```
+
+##### User
+
+To configure define `SV4GIT_HOME` environment variable, eg.:
+
+```bash
+SV4GIT_HOME=/home/myuser/.sv4git # myuser is just an example
+```
+
+And define the `config.yml` inside it, eg:
+
+```bash
+.sv4git
+└── config.yml
+```
+
+##### Repository
+
+Create a `.sv4git.yml` on the root of your repository, eg.: [.sv4git.yml](.sv4git.yml)
+
+#### Configuration format
+
+```yml
+version: "1.0" #config version
+
+versioning: # versioning bump
+ update-major: [] # commit types used to bump major
+ update-minor: # commit types used to bump minor
+ - feat
+ update-patch: # commit types used to bump patch
+ - build
+ - ci
+ - chore
+ - docs
+ - fix
+ - perf
+ - refactor
+ - style
+ - test
+ # when type is not present on update rules and is unknown (not mapped on commit message types),
+ # if ignore-unknown=false bump patch, if ignore-unknown=true do not bump version
+ ignore-unknown: false
+
+tag:
+ pattern: '%d.%d.%d' # pattern used to create git tag
+
+release-notes:
+ headers: # headers names for relase notes markdown, to disable a section, just remove the header line
+ breaking-change: Breaking Changes
+ feat: Features
+ fix: Bug Fixes
+
+branches: # git branches config
+ prefix: ([a-z]+\/)? # prefix used on branch name, should be a regex group
+ suffix: (-.*)? # suffix used on branch name, should be a regex group
+ disable-issue: false # set true if there is no need to recover issue id from branch name
+ skip: # list of branch names ignored on commit message validation
+ - master
+ - main
+ - developer
+
+commit-message:
+ types: # supported commit types
+ - build
+ - ci
+ - chore
+ - docs
+ - feat
+ - fix
+ - perf
+ - refactor
+ - revert
+ - style
+ - test
+ 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
+ values: []
+ footer:
+ issue:
+ key: jira # name used to define an issue on footer metadata
+ key-synonyms: # supported variations for footer metadata
+ - Jira
+ - JIRA
+ use-hash: false # if false, use : separator, if true, use # separator
+ issue:
+ regex: '[A-Z]+-[0-9]+' # regex for issue id
+```
### Running
@@ -48,7 +137,7 @@ git sv next-version
#### Usage
-use `--help` or `-h` to get usage information, dont forget that some commands have unique options too
+use `--help` or `-h` to get usage information, don't forget that some commands have unique options too
```bash
# sv help
From fd01367a1f825707061b8b053afc0babb2b483bb Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Mon, 15 Feb 2021 03:31:56 -0300
Subject: [PATCH 22/24] docs: add config on available commands
---
README.md | 25 +++++++++++++------------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/README.md b/README.md
index 2a539fe..020b2a4 100644
--- a/README.md
+++ b/README.md
@@ -149,18 +149,19 @@ git-sv rn -h
##### Available commands
-| Variable | description | has options |
-| ---------------------------- | ------------------------------------------------------------- | :----------------: |
-| current-version, cv | get last released version from git | :x: |
-| next-version, nv | generate the next version based on git commit messages | :x: |
-| commit-log, cl | list all commit logs according to range as jsons | :heavy_check_mark: |
-| commit-notes, cn | generate a commit notes according to range | :heavy_check_mark: |
-| release-notes, rn | generate release notes | :heavy_check_mark: |
-| changelog, cgl | generate changelog | :heavy_check_mark: |
-| tag, tg | generate tag with version based on git commit messages | :x: |
-| commit, cmt | execute git commit with convetional commit message helper | :x: |
-| validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: |
-| help, h | shows a list of commands or help for one command | :x: |
+| Variable | description | has options or subcommands |
+| ---------------------------- | ------------------------------------------------------------- | :------------------------: |
+| config, cfg | show config information | :heavy_check_mark: |
+| current-version, cv | get last released version from git | :x: |
+| next-version, nv | generate the next version based on git commit messages | :x: |
+| commit-log, cl | list all commit logs according to range as jsons | :heavy_check_mark: |
+| commit-notes, cn | generate a commit notes according to range | :heavy_check_mark: |
+| release-notes, rn | generate release notes | :heavy_check_mark: |
+| changelog, cgl | generate changelog | :heavy_check_mark: |
+| tag, tg | generate tag with version based on git commit messages | :x: |
+| commit, cmt | execute git commit with convetional commit message helper | :x: |
+| validate-commit-message, vcm | use as prepare-commit-message hook to validate commit message | :heavy_check_mark: |
+| help, h | shows a list of commands or help for one command | :x: |
##### Use range
From f59c0ae290bce280e69749f5bcbffbcebc9da032 Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Wed, 17 Feb 2021 21:17:32 -0300
Subject: [PATCH 23/24] docs: add badges on readme
---
README.md | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 020b2a4..2eac79c 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,15 @@
-# sv4git
-
-Semantic version for git
+
+
sv4git
+ semantic version for git cli
+
+
+
+
+
+
+
+
+
## Getting Started
From 0d35a3113eec0cf5bc3ca86ecb3066819df9ba5a Mon Sep 17 00:00:00 2001
From: Beatriz Vieira
Date: Wed, 17 Feb 2021 21:18:46 -0300
Subject: [PATCH 24/24] docs: fix description
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 2eac79c..fce07fc 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
sv4git
- semantic version for git cli
+ semantic version for git