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

+

+ Release + GitHub stars + Software License + GitHub Actions + Go Report Card + Software License +

+

## 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

Release GitHub stars