git-chglog/tag_reader.go
Louis DeLosSantos ebff3d0beb
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>
2021-03-20 17:21:47 -05:00

163 lines
3.2 KiB
Go

package chglog
import (
"fmt"
"regexp"
"sort"
"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, sort string) *tagReader {
return &tagReader{
client: client,
separator: "@@__CHGLOG__@@",
reFilter: regexp.MustCompile(filterPattern),
sortBy: sort,
}
}
func (r *tagReader) ReadAll() ([]*Tag, error) {
out, err := r.client.Exec(
"for-each-ref",
"--format",
"%(refname)"+r.separator+"%(subject)"+r.separator+"%(taggerdate)"+r.separator+"%(authordate)",
"refs/tags",
)
tags := []*Tag{}
if err != nil {
return tags, fmt.Errorf("failed to get git-tag: %w", err)
}
lines := strings.Split(out, "\n")
for _, line := range lines {
tokens := strings.Split(line, r.separator)
if len(tokens) != 4 {
continue
}
name := r.parseRefname(tokens[0])
subject := r.parseSubject(tokens[1])
date, err := r.parseDate(tokens[2])
if err != nil {
t, err2 := r.parseDate(tokens[3])
if err2 != nil {
return nil, err2
}
date = t
}
if r.reFilter != nil {
if !r.reFilter.MatchString(name) {
continue
}
}
tags = append(tags, &Tag{
Name: name,
Subject: subject,
Date: date,
})
}
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)
}
func (*tagReader) parseSubject(input string) string {
return strings.TrimSpace(input)
}
func (*tagReader) parseDate(input string) (time.Time, error) {
return time.Parse("Mon Jan 2 15:04:05 2006 -0700", input)
}
func (*tagReader) assignPreviousAndNextTag(tags []*Tag) {
total := len(tags)
for i, tag := range tags {
var (
next *RelateTag
prev *RelateTag
)
if i > 0 {
next = &RelateTag{
Name: tags[i-1].Name,
Subject: tags[i-1].Subject,
Date: tags[i-1].Date,
}
}
if i+1 < total {
prev = &RelateTag{
Name: tags[i+1].Name,
Subject: tags[i+1].Subject,
Date: tags[i+1].Date,
}
}
tag.Next = next
tag.Previous = prev
}
}
func (*tagReader) sortTags(tags []*Tag) {
sort.Slice(tags, func(i, j int) bool {
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)
})
}