From ebff3d0beb8ce113edf0e62f3ca3c59a2ec20b05 Mon Sep 17 00:00:00 2001 From: Louis DeLosSantos Date: Sat, 20 Mar 2021 18:21:47 -0400 Subject: [PATCH] feat: allow tag sorting by semver (#124) Relates to #123. While this does not introduce "per-branch" tag parsing it does allow an alternative tag sorting method which maybe a better solution. With this commit the user can decide to sort the tags by semver instead of dates. This is useful where repositories are utilizing a stable branch model and back-ports are interleaved with new releases. For example, if your mainline is on v3.0.0 with it's last release 1/1/2021 and a back-port release of v2.0.1 is released on 1/2/2021, sorting by semver will correctly order the change log producing v2.0.1 -> v2.0.1 -> v3.0.0 This functionality is completely opt-in and defaults to the original "date" sorting Signed-off-by: ldelossa --- README.md | 9 +++ chglog.go | 3 +- chglog_test.go | 2 + cmd/git-chglog/config.go | 16 +++++ go.mod | 1 + go.sum | 2 + tag_reader.go | 41 ++++++++++++- tag_reader_test.go | 125 ++++++++++++++++++++++++++++++++++++--- 8 files changed, 187 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4f0d0204..ae43bbaf 100644 --- a/README.md +++ b/README.md @@ -263,6 +263,7 @@ info: options: tag_filter_pattern: '^v' + sort: "date" commits: filters: @@ -347,6 +348,14 @@ so it is recommended to specify it. Options used to process commits. +#### `options.sort` + +Options concerning the acquisition and sort of commits. + +| Required | Type | Default | Description | +|:---------|:------------|:----------|:--------------------------------------------------------------------------------------------------------------------| +| N | String | `"date"` | Defines how tags are sorted in the generated change log. Values: "date", "semver". | + #### `options.commits` Options concerning the acquisition and sort of commits. diff --git a/chglog.go b/chglog.go index f71f8095..68c8b3f3 100644 --- a/chglog.go +++ b/chglog.go @@ -20,6 +20,7 @@ type Options struct { Processor Processor NextTag string // Treat unreleased commits as specified tags (EXPERIMENTAL) TagFilterPattern string // Filter tag by regexp + Sort string // Specify how to sort tags; currently supports "date" (default) or by "semver". NoCaseSensitive bool // Filter commits in a case insensitive way CommitFilters map[string][]string // Filter by using `Commit` properties and values. Filtering is not done by specifying an empty value CommitSortBy string // Property name to use for sorting `Commit` (e.g. `Scope`) @@ -120,7 +121,7 @@ func NewGenerator(logger *Logger, config *Config) *Generator { return &Generator{ client: client, config: config, - tagReader: newTagReader(client, config.Options.TagFilterPattern), + tagReader: newTagReader(client, config.Options.TagFilterPattern, config.Options.Sort), tagSelector: newTagSelector(), commitParser: newCommitParser(logger, client, jiraClient, config), commitExtractor: newCommitExtractor(config.Options), diff --git a/chglog_test.go b/chglog_test.go index 47082a36..3c1486bc 100644 --- a/chglog_test.go +++ b/chglog_test.go @@ -215,6 +215,7 @@ change message.`) RepositoryURL: "https://github.com/git-chglog/git-chglog", }, Options: &Options{ + Sort: "date", CommitFilters: map[string][]string{ "Type": { "feat", @@ -329,6 +330,7 @@ func TestGeneratorWithNextTag(t *testing.T) { RepositoryURL: "https://github.com/git-chglog/git-chglog", }, Options: &Options{ + Sort: "date", NextTag: "3.0.0", CommitFilters: map[string][]string{ "Type": { diff --git a/cmd/git-chglog/config.go b/cmd/git-chglog/config.go index b3992b86..f00985ba 100644 --- a/cmd/git-chglog/config.go +++ b/cmd/git-chglog/config.go @@ -72,6 +72,7 @@ type JiraOptions struct { // Options ... type Options struct { TagFilterPattern string `yaml:"tag_filter_pattern"` + Sort string `yaml:"sort"` Commits CommitOptions `yaml:"commits"` CommitGroups CommitGroupOptions `yaml:"commit_groups"` Header PatternOptions `yaml:"header"` @@ -122,6 +123,7 @@ func (config *Config) Normalize(ctx *CLIContext) error { } config.normalizeStyle() + config.normalizeTagSortBy() return nil } @@ -138,6 +140,19 @@ func (config *Config) normalizeStyle() { } } +func (config *Config) normalizeTagSortBy() { + switch { + case config.Options.Sort == "": + config.Options.Sort = "date" + case strings.EqualFold(config.Options.Sort, "date"): + config.Options.Sort = "date" + case strings.EqualFold(config.Options.Sort, "semver"): + config.Options.Sort = "semver" + default: + config.Options.Sort = "date" + } +} + // For GitHub func (config *Config) normalizeStyleOfGitHub() { opts := config.Options @@ -293,6 +308,7 @@ func (config *Config) Convert(ctx *CLIContext) *chglog.Config { Options: &chglog.Options{ NextTag: ctx.NextTag, TagFilterPattern: ctx.TagFilterPattern, + Sort: opts.Sort, NoCaseSensitive: ctx.NoCaseSensitive, Paths: ctx.Paths, CommitFilters: opts.Commits.Filters, diff --git a/go.mod b/go.mod index b9a8a7e3..c56b0ef7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/AlecAivazis/survey/v2 v2.2.9 github.com/andygrunwald/go-jira v1.13.0 + github.com/coreos/go-semver v0.3.0 github.com/fatih/color v1.10.0 github.com/imdario/mergo v0.3.12 github.com/kyokomi/emoji/v2 v2.2.8 diff --git a/go.sum b/go.sum index 2e16e071..c6593849 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nB github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc= github.com/andygrunwald/go-jira v1.13.0 h1:vvIImGgX32bHfoiyUwkNo+/YrPnRczNarvhLOncP6dE= github.com/andygrunwald/go-jira v1.13.0/go.mod h1:jYi4kFDbRPZTJdJOVJO4mpMMIwdB+rcZwSO58DzPd2I= +github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/tag_reader.go b/tag_reader.go index 39486073..754cb633 100644 --- a/tag_reader.go +++ b/tag_reader.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/coreos/go-semver/semver" gitcmd "github.com/tsuyoshiwada/go-gitcmd" ) @@ -14,13 +15,15 @@ type tagReader struct { client gitcmd.Client separator string reFilter *regexp.Regexp + sortBy string } -func newTagReader(client gitcmd.Client, filterPattern string) *tagReader { +func newTagReader(client gitcmd.Client, filterPattern string, sort string) *tagReader { return &tagReader{ client: client, separator: "@@__CHGLOG__@@", reFilter: regexp.MustCompile(filterPattern), + sortBy: sort, } } @@ -71,12 +74,36 @@ func (r *tagReader) ReadAll() ([]*Tag, error) { }) } - r.sortTags(tags) + switch r.sortBy { + case "date": + r.sortTags(tags) + case "semver": + r.filterSemVerTags(&tags) + r.sortTagsBySemver(tags) + } r.assignPreviousAndNextTag(tags) return tags, nil } +func (*tagReader) filterSemVerTags(tags *[]*Tag) { + // filter out any non-semver tags + for i, t := range *tags { + // remove leading v, since its so + // common. + name := t.Name + if strings.HasPrefix(name, "v") { + name = strings.TrimPrefix(name, "v") + } + + // attempt semver parse, if not successful + // remove it from tags slice. + if _, err := semver.NewVersion(name); err != nil { + *tags = append((*tags)[:i], (*tags)[i+1:]...) + } + } +} + func (*tagReader) parseRefname(input string) string { return strings.Replace(input, "refs/tags/", "", 1) } @@ -124,3 +151,13 @@ func (*tagReader) sortTags(tags []*Tag) { return !tags[i].Date.Before(tags[j].Date) }) } + +func (*tagReader) sortTagsBySemver(tags []*Tag) { + sort.Slice(tags, func(i, j int) bool { + semver1 := strings.TrimPrefix(tags[i].Name, "v") + semver2 := strings.TrimPrefix(tags[j].Name, "v") + v1 := semver.New(semver1) + v2 := semver.New(semver2) + return v2.LessThan(*v1) + }) +} diff --git a/tag_reader_test.go b/tag_reader_test.go index 786951a5..43cd308d 100644 --- a/tag_reader_test.go +++ b/tag_reader_test.go @@ -21,6 +21,7 @@ func TestTagReader(t *testing.T) { "refs/tags/v2.0.4-beta.1@@__CHGLOG__@@Release v2.0.4-beta.1@@__CHGLOG__@@Thu Feb 1 00:00:00 2018 +0000@@__CHGLOG__@@", "refs/tags/4.4.3@@__CHGLOG__@@This is tag subject@@__CHGLOG__@@@@__CHGLOG__@@Fri Feb 2 00:00:00 2018 +0000", "refs/tags/4.4.4@@__CHGLOG__@@Release 4.4.4@@__CHGLOG__@@Fri Feb 2 10:00:40 2018 +0000@@__CHGLOG__@@", + "refs/tags/v2.0.4-beta.2@@__CHGLOG__@@Release v2.0.4-beta.2@@__CHGLOG__@@Sat Feb 3 12:15:00 2018 +0000@@__CHGLOG__@@", "refs/tags/5.0.0-rc.0@@__CHGLOG__@@Release 5.0.0-rc.0@@__CHGLOG__@@Sat Feb 3 12:30:10 2018 +0000@@__CHGLOG__@@", "refs/tags/hoge_fuga@@__CHGLOG__@@Invalid semver tag name@@__CHGLOG__@@Mon Mar 12 12:30:10 2018 +0000@@__CHGLOG__@@", "hoge@@__CHGLOG__@@", @@ -28,7 +29,7 @@ func TestTagReader(t *testing.T) { }, } - actual, err := newTagReader(client, "").ReadAll() + actual, err := newTagReader(client, "", "date").ReadAll() assert.Nil(err) assert.Equal( @@ -53,6 +54,21 @@ func TestTagReader(t *testing.T) { Subject: "Invalid semver tag name", Date: time.Date(2018, 3, 12, 12, 30, 10, 0, time.UTC), }, + Previous: &RelateTag{ + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + }, + }, + { + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + Next: &RelateTag{ + Name: "5.0.0-rc.0", + Subject: "Release 5.0.0-rc.0", + Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC), + }, Previous: &RelateTag{ Name: "4.4.4", Subject: "Release 4.4.4", @@ -64,9 +80,9 @@ func TestTagReader(t *testing.T) { Subject: "Release 4.4.4", Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC), Next: &RelateTag{ - Name: "5.0.0-rc.0", - Subject: "Release 5.0.0-rc.0", - Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC), + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), }, Previous: &RelateTag{ Name: "4.4.3", @@ -104,15 +120,106 @@ func TestTagReader(t *testing.T) { actual, ) - actualFiltered, errFiltered := newTagReader(client, "^v").ReadAll() + actual, err = newTagReader(client, "", "semver").ReadAll() + assert.Nil(err) + + assert.Equal( + []*Tag{ + { + Name: "5.0.0-rc.0", + Subject: "Release 5.0.0-rc.0", + Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC), + Next: nil, + Previous: &RelateTag{ + Name: "4.4.4", + Subject: "Release 4.4.4", + Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC), + }, + }, + { + Name: "4.4.4", + Subject: "Release 4.4.4", + Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC), + Next: &RelateTag{ + Name: "5.0.0-rc.0", + Subject: "Release 5.0.0-rc.0", + Date: time.Date(2018, 2, 3, 12, 30, 10, 0, time.UTC), + }, + Previous: &RelateTag{ + Name: "4.4.3", + Subject: "This is tag subject", + Date: time.Date(2018, 2, 2, 0, 0, 0, 0, time.UTC), + }, + }, + { + Name: "4.4.3", + Subject: "This is tag subject", + Date: time.Date(2018, 2, 2, 0, 0, 0, 0, time.UTC), + Next: &RelateTag{ + Name: "4.4.4", + Subject: "Release 4.4.4", + Date: time.Date(2018, 2, 2, 10, 0, 40, 0, time.UTC), + }, + Previous: &RelateTag{ + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + }, + }, + { + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + Next: &RelateTag{ + Name: "4.4.3", + Subject: "This is tag subject", + Date: time.Date(2018, 2, 2, 0, 0, 0, 0, time.UTC), + }, + Previous: &RelateTag{ + Name: "v2.0.4-beta.1", + Subject: "Release v2.0.4-beta.1", + Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + Name: "v2.0.4-beta.1", + Subject: "Release v2.0.4-beta.1", + Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + Next: &RelateTag{ + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + }, + Previous: nil, + }, + }, + actual, + ) + + actualFiltered, errFiltered := newTagReader(client, "^v", "date").ReadAll() assert.Nil(errFiltered) assert.Equal( []*Tag{ { - Name: "v2.0.4-beta.1", - Subject: "Release v2.0.4-beta.1", - Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), - Next: nil, + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + Next: nil, + Previous: &RelateTag{ + Name: "v2.0.4-beta.1", + Subject: "Release v2.0.4-beta.1", + Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + Name: "v2.0.4-beta.1", + Subject: "Release v2.0.4-beta.1", + Date: time.Date(2018, 2, 1, 0, 0, 0, 0, time.UTC), + Next: &RelateTag{ + Name: "v2.0.4-beta.2", + Subject: "Release v2.0.4-beta.2", + Date: time.Date(2018, 2, 3, 12, 15, 0, 0, time.UTC), + }, Previous: nil, }, },