mirror of
https://github.com/git-chglog/git-chglog.git
synced 2026-01-22 18:06:11 +00:00
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:
parent
9a1a9a525c
commit
ebff3d0beb
8 changed files with 187 additions and 12 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
1
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue