Skip to content

Commit

Permalink
feat: allow tag sorting by semver (#124)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
ldelossa committed Mar 20, 2021
1 parent 9a1a9a5 commit ebff3d0
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 12 deletions.
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -263,6 +263,7 @@ info:

options:
tag_filter_pattern: '^v'
sort: "date"

commits:
filters:
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion chglog.go
Expand Up @@ -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`)
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions chglog_test.go
Expand Up @@ -215,6 +215,7 @@ change message.`)
RepositoryURL: "https://github.com/git-chglog/git-chglog",
},
Options: &Options{
Sort: "date",
CommitFilters: map[string][]string{
"Type": {
"feat",
Expand Down Expand Up @@ -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": {
Expand Down
16 changes: 16 additions & 0 deletions cmd/git-chglog/config.go
Expand Up @@ -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"`
Expand Down Expand Up @@ -122,6 +123,7 @@ func (config *Config) Normalize(ctx *CLIContext) error {
}

config.normalizeStyle()
config.normalizeTagSortBy()

return nil
}
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Expand Up @@ -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=
Expand Down
41 changes: 39 additions & 2 deletions tag_reader.go
Expand Up @@ -7,20 +7,23 @@ import (
"strings"
"time"

"github.com/coreos/go-semver/semver"
gitcmd "github.com/tsuyoshiwada/go-gitcmd"
)

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

Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
})
}
125 changes: 116 additions & 9 deletions tag_reader_test.go
Expand Up @@ -21,14 +21,15 @@ 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__@@",
}, "\n"), nil
},
}

actual, err := newTagReader(client, "").ReadAll()
actual, err := newTagReader(client, "", "date").ReadAll()
assert.Nil(err)

assert.Equal(
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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,
},
},
Expand Down

0 comments on commit ebff3d0

Please sign in to comment.