diff --git a/go.mod b/go.mod index b6509dd..26768c8 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,19 @@ module github.com/syncthing/github-release-tool require ( github.com/alecthomas/kong v0.2.12 github.com/google/go-github v17.0.0+incompatible - github.com/google/go-querystring v1.0.0 // indirect - github.com/stretchr/testify v1.5.1 // indirect + golang.org/x/mod v0.21.0 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be +) + +require ( + github.com/golang/protobuf v1.3.1 // indirect + github.com/google/go-querystring v1.0.0 // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/stretchr/testify v1.8.4 // indirect + golang.org/x/net v0.15.0 // indirect google.golang.org/appengine v1.6.5 // indirect ) -go 1.13 +go 1.22.0 + +toolchain go1.23.1 diff --git a/go.sum b/go.sum index 9edb3d1..17424be 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/alecthomas/kong v0.2.12 h1:X3kkCOXGUNzLmiu+nQtoxWqj4U2a39MpSJR3QdQXOwI= github.com/alecthomas/kong v0.2.12/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= @@ -14,13 +12,15 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -29,7 +29,5 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/grt/cmd_semver.go b/grt/cmd_semver.go new file mode 100644 index 0000000..6d892bf --- /dev/null +++ b/grt/cmd_semver.go @@ -0,0 +1,147 @@ +package main + +import ( + "cmp" + "context" + "fmt" + "slices" + "strconv" + "strings" + + "github.com/google/go-github/github" + "golang.org/x/mod/semver" +) + +type semverCmd struct{} + +func (cmd *semverCmd) Run(o *commonOptions) error { + ctx := context.Background() + tags, _, err := o.client.Repositories.ListTags(ctx, o.Owner, o.Repo, &github.ListOptions{}) + if err != nil { + return err + } + + // Get the latest tag + var latestTag *github.RepositoryTag + for _, tag := range tags { + if !semver.IsValid(tag.GetName()) { + continue + } + if semver.Prerelease(tag.GetName()) != "" { + continue + } + if latestTag == nil { + latestTag = tag + continue + } + if semver.Compare(tag.GetName(), latestTag.GetName()) > 0 { + latestTag = tag + } + } + + fmt.Println("Latest tag:", latestTag.GetName()) + + // Get the commits since that tag + comp, _, err := o.client.Repositories.CompareCommits(ctx, o.Owner, o.Repo, latestTag.GetCommit().GetSHA(), "main") + if err != nil { + return err + } + + // Print the commits + bumpMinor := false + var commits []conventionalCommit + for _, commit := range comp.Commits { + msg := commit.GetCommit().GetMessage() + msg, _, _ = strings.Cut(msg, "\n") + cc, ok := parseConventionalCommit(msg) + if !ok { + fmt.Println("-", commit.GetSHA(), msg) + continue + } + commits = append(commits, cc) + fmt.Println("+", commit.GetSHA(), cc.kind, cc.scopes, cc.description) + bumpMinor = bumpMinor || cc.feature() + } + + ver := parseVer(latestTag.GetName()) + if bumpMinor { + ver[1]++ + ver[2] = 0 + } else { + ver[2]++ + } + + fmt.Println(formatVer(ver)) + fmt.Println(formatCommitList(commits)) + + return nil +} + +func formatCommitList(commits []conventionalCommit) string { + slices.SortFunc(commits, func(a, b conventionalCommit) int { + if len(a.scopes) > 0 && len(b.scopes) > 0 { + if d := cmp.Compare(a.scopes[0], b.scopes[0]); d != 0 { + return d + } + } + return cmp.Compare(a.description, b.description) + }) + + var fixes, features, others []conventionalCommit + for _, cc := range commits { + if cc.fix() { + fixes = append(fixes, cc) + } else if cc.feature() { + features = append(features, cc) + } else { + others = append(others, cc) + } + } + + var b strings.Builder + if len(fixes) > 0 { + b.WriteString("## Bugfixes:\n") + for _, cc := range fixes { + b.WriteString("- ") + b.WriteString(cc.messageString()) + b.WriteRune('\n') + } + b.WriteRune('\n') + } + if len(features) > 0 { + b.WriteString("## Features:\n") + for _, cc := range features { + b.WriteString("- ") + b.WriteString(cc.messageString()) + b.WriteRune('\n') + } + b.WriteRune('\n') + } + if len(others) > 0 { + b.WriteString("## Other things:\n") + for _, cc := range others { + b.WriteString("- ") + b.WriteString(cc.messageString()) + b.WriteRune('\n') + } + b.WriteRune('\n') + } + return b.String() +} + +func parseVer(v string) []int { + var ver []int + v = strings.TrimPrefix(v, "v") + for _, s := range strings.Split(v, ".") { + d, _ := strconv.Atoi(s) + ver = append(ver, d) + } + for len(ver) < 3 { + ver = append(ver, 0) + } + return ver +} + +func formatVer(ver []int) string { + return fmt.Sprintf("v%d.%d.%d", ver[0], ver[1], ver[2]) +} diff --git a/grt/conventionalcommits.go b/grt/conventionalcommits.go new file mode 100644 index 0000000..54761d2 --- /dev/null +++ b/grt/conventionalcommits.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "regexp" + "slices" + "strings" +) + +var ccExp = regexp.MustCompile(`^(?P\w+)(?:\((?P.+)\))?!?: (?P.+)$`) + +type conventionalCommit struct { + kind string + scopes []string + description string +} + +func (c conventionalCommit) feature() bool { + return c.kind == "feat" +} + +func (c conventionalCommit) fix() bool { + return c.kind == "fix" +} + +func (c conventionalCommit) messageString() string { + sentenceDescr := strings.ToUpper(c.description[:1]) + c.description[1:] + if len(c.scopes) == 0 { + return sentenceDescr + } + return fmt.Sprintf("_%s:_ %s", strings.Join(c.scopes, ", "), sentenceDescr) +} + +func parseConventionalCommit(msg string) (conventionalCommit, bool) { + matches := ccExp.FindStringSubmatch(msg) + if matches == nil { + return conventionalCommit{}, false + } + + cc := conventionalCommit{ + kind: matches[1], + description: matches[3], + } + if matches[2] != "" { + cc.scopes = strings.Split(matches[2], ",") + for i := range cc.scopes { + cc.scopes[i] = strings.TrimSpace(cc.scopes[i]) + } + slices.Sort(cc.scopes) + } + return cc, true +} diff --git a/grt/conventionalcommits_test.go b/grt/conventionalcommits_test.go new file mode 100644 index 0000000..abd8300 --- /dev/null +++ b/grt/conventionalcommits_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "slices" + "testing" +) + +func TestParseConventionalCommits(t *testing.T) { + cases := []struct { + input string + kind string + scope []string + description string + ok bool + }{ + {"feat: add new feature", "feat", nil, "add new feature", true}, + {"feat(scope): add new feature", "feat", []string{"scope"}, "add new feature", true}, + {"feat(scope1, scope2): add new feature", "feat", []string{"scope1", "scope2"}, "add new feature", true}, + {"lib/foo: whatever", "", nil, "", false}, + } + + for _, c := range cases { + cc, ok := parseConventionalCommit(c.input) + if ok != c.ok || cc.kind != c.kind || !slices.Equal(cc.scopes, c.scope) || cc.description != c.description { + t.Errorf("parseConventionalCommit(%q) == %q, %v, %q, %v, want %q, %v, %q, %v", c.input, cc.kind, cc.scopes, cc.description, ok, c.kind, c.scope, c.description, c.ok) + } + } +} diff --git a/grt/main.go b/grt/main.go index b4ad247..59f978d 100644 --- a/grt/main.go +++ b/grt/main.go @@ -25,6 +25,7 @@ type cliOptions struct { Milestone milestoneOptions `cmd:"" help:"Collect resolved issues into milestone"` Changelog changelogOptions `cmd:"" help:"Show changelog for milestone"` Release releaseOptions `cmd:"" help:"Create release from milestone"` + Semver semverCmd `cmd:"" help:"Show latest tag"` } type commonOptions struct { @@ -93,7 +94,6 @@ func (o *milestoneOptions) Run(common *commonOptions) error { func (o changelogOptions) Run(common *commonOptions) error { return changelog(common.ctx, os.Stdout, common.client, common.Owner, common.Repo, o.Release, o.Md, o.SkipLabels, true) - } func (o releaseOptions) Run(common *commonOptions) error { @@ -288,7 +288,7 @@ func createRelease(ctx context.Context, client *github.Client, owner, repo, rele State: github.String("closed"), }) if err != nil { - return fmt.Errorf("closing milestone: %w") + return fmt.Errorf("closing milestone: %w", err) } } } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy