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 <louis.delos@gmail.com>
This commit is contained in:
Louis DeLosSantos 2021-03-20 18:21:47 -04:00 committed by GitHub
parent 9a1a9a525c
commit ebff3d0beb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 187 additions and 12 deletions

View file

@ -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.

View file

@ -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),

View file

@ -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": {

View file

@ -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,

1
go.mod
View file

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

2
go.sum
View file

@ -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=

View file

@ -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)
})
}

View file

@ -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,
},
},