From 94f83cca6a95c781259047f457b94915b84a6ce4 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 30 Sep 2024 14:48:56 -0500 Subject: [PATCH 1/2] wip --- go.mod | 15 +++- go.sum | 18 ++-- grt/cmd_semver.go | 147 ++++++++++++++++++++++++++++++++ grt/conventionalcommits.go | 50 +++++++++++ grt/conventionalcommits_test.go | 28 ++++++ grt/main.go | 4 +- 6 files changed, 247 insertions(+), 15 deletions(-) create mode 100644 grt/cmd_semver.go create mode 100644 grt/conventionalcommits.go create mode 100644 grt/conventionalcommits_test.go 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..693bb56 --- /dev/null +++ b/grt/conventionalcommits.go @@ -0,0 +1,50 @@ +package main + +import ( + "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 { + if len(c.scopes) == 0 { + return c.description + } + return strings.Join(c.scopes, ", ") + ": " + c.description +} + +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) } } } From 438ea1c4e6ebd7bbd875b74aa6d5f1f1f3190001 Mon Sep 17 00:00:00 2001 From: Jakob Borg Date: Mon, 31 Mar 2025 09:34:17 +0200 Subject: [PATCH 2/2] wip --- grt/conventionalcommits.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/grt/conventionalcommits.go b/grt/conventionalcommits.go index 693bb56..54761d2 100644 --- a/grt/conventionalcommits.go +++ b/grt/conventionalcommits.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "regexp" "slices" "strings" @@ -23,10 +24,11 @@ func (c conventionalCommit) fix() bool { } func (c conventionalCommit) messageString() string { + sentenceDescr := strings.ToUpper(c.description[:1]) + c.description[1:] if len(c.scopes) == 0 { - return c.description + return sentenceDescr } - return strings.Join(c.scopes, ", ") + ": " + c.description + return fmt.Sprintf("_%s:_ %s", strings.Join(c.scopes, ", "), sentenceDescr) } func parseConventionalCommit(msg string) (conventionalCommit, bool) { 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