Changelog Overhaul 2 #19

Merged
zeripath merged 25 commits from jolheiser/changelog:gitea-sdk into master 2020-01-24 16:30:19 +00:00
18 changed files with 663 additions and 239 deletions

View File

@ -1,3 +1,7 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
// +build ignore
package main
@ -10,8 +14,12 @@ import (
const (
exampleFile = "changelog.example.yml"
writeFile = "config_default.go"
tmpl = `package main
writeFile = "config/config_default.go"
tmpl = `// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
func init() {
defaultConfig = []byte(` + "`" + `%s` + "`" + `)

View File

@ -1,6 +1,13 @@
# The full repository name
Review

copyright head ? @lunny

copyright head ? @lunny
repo: go-gitea/gitea
# Service type (gitea or github)
service: github
# Base URL for Gitea instance if using gitea service type (optional)
# Default: https://gitea.com

If the example base-url points to gitea.com, shouldn't the service match it with gitea instead of github?

If the example `base-url` points to gitea.com, shouldn't the service match it with `gitea` instead of `github`?

Should we really attempt to infer service type based on this URL?

Should we really attempt to infer service type based on this URL?

I didn't mean it like that. I thought this file would make more sense either like:

# Service type (gitea or github)
service: gitea

# Base URL for Gitea instance if using gitea service type
base-url: https://gitea.com

or

# Service type (gitea or github)
service: github

# Base URL for Gitea instance if using GitHub service type
base-url: https://github.com

Currently it looks mixed up: service says github but URL is gitea.com (if it's intentional, I'm missing the point)

I didn't mean it like that. I thought this file would make more sense either like: ``` # Service type (gitea or github) service: gitea # Base URL for Gitea instance if using gitea service type base-url: https://gitea.com ``` or ``` # Service type (gitea or github) service: github # Base URL for Gitea instance if using GitHub service type base-url: https://github.com ``` Currently it looks mixed up: service says `github` but URL is `gitea.com` (if it's intentional, I'm missing the point)

Ah, no that's okay.
The reason I did it this way is because https://gitea.com is the default gitea instance, yet currently we are still primarily using GitHub, so I left the service set as such.
No need to set a base-url for GitHub because it will never change.

Ah, no that's okay. The reason I did it this way is because `https://gitea.com` is the default gitea instance, yet currently we are still primarily using GitHub, so I left the service set as such. No need to set a base-url for GitHub because it will never change.

It's only a bit confusing, that's all. Being the example file...

It's only a bit confusing, that's all. Being the example file...

Yeah, fair enough. Do you think it would make more sense to leave base-url blank for the time being?

Yeah, fair enough. Do you think it would make more sense to leave `base-url` blank for the time being?

I'd use Gitea's for both parameters and add a comment about base-url not being required in the case of GitHub. Having said that, I would honor any base-url setting, even in the case of GitHub. We never know, maybe there's another repository hosting service that implements GitHub's API that we never knew about, or maybe GitHub allows private domains for corporate accounts, etc.

Ideally, I'd make base-url optional in both cases (GitHub and Gitea), but always honor its value if present and not empty.

I'd use Gitea's for both parameters and add a comment about `base-url` not being required in the case of GitHub. Having said that, I _would_ honor any `base-url` setting, even in the case of GitHub. We never know, maybe there's another repository hosting service that implements GitHub's API that we never knew about, or maybe GitHub allows private domains for corporate accounts, etc. Ideally, I'd make `base-url` optional in _both cases_ (GitHub and Gitea), but always honor its value if present and not empty.
base-url:
# Changelog groups and which labeled PRs to add to each group
groups:
-

13
cmd/cmd.go Normal file
View File

@ -0,0 +1,13 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
var (
MilestoneFlag string
ConfigPathFlag string
TokenFlag string
DetailsFlag bool
AfterFlag int64
)

46
cmd/contributors.go Normal file
View File

@ -0,0 +1,46 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"sort"
"code.gitea.io/changelog/config"
"code.gitea.io/changelog/service"
"github.com/urfave/cli/v2"
)
var Contributors = &cli.Command{
Name: "contributors",
Usage: "Generates a contributors list",
Action: runContributors,
}
func runContributors(cmd *cli.Context) error {
cfg, err := config.New(ConfigPathFlag)
if err != nil {
return err
}
s, err := service.New(cfg.Service, cfg.Repo, cfg.BaseURL, MilestoneFlag, TokenFlag)
if err != nil {
return err
}
contributors, err := s.Contributors()
if err != nil {
return err
}
sort.Sort(contributors)
for _, contributor := range contributors {
fmt.Printf("* [@%s](%s)\n", contributor.Name, contributor.Profile)
}
return nil
}

101
cmd/generate.go Normal file
View File

@ -0,0 +1,101 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package cmd
import (
"fmt"
"code.gitea.io/changelog/config"
"code.gitea.io/changelog/service"
"github.com/urfave/cli/v2"
)
var Generate = &cli.Command{
Name: "generate",
Usage: "Generates a changelog",

If changelog here is a parameter, it's customary to use some indication (e.g. generate {changelog}). In *nix style, curly braces mean required parameters and square brackets mean optional. If you prefer, you could use another style like generate "changelog".

If `changelog` here is a parameter, it's customary to use some indication (e.g. `generate {changelog}`). In *nix style, curly braces mean required parameters and square brackets mean optional. If you prefer, you could use another style like `generate "changelog"`.

I can change it, this isn't a parameter.

I can change it, this isn't a parameter.

I was wondering about that. No need to change, then. ?

I was wondering about that. No need to change, then. ?
Action: runGenerate,
}
func runGenerate(cmd *cli.Context) error {
cfg, err := config.New(ConfigPathFlag)
if err != nil {
return err
}
labels := make(map[string]string)
entries := make(map[string][]service.PullRequest)
var defaultGroup string
for _, g := range cfg.Groups {
entries[g.Name] = []service.PullRequest{}
for _, l := range g.Labels {
labels[l] = g.Name
}
if g.Default {
defaultGroup = g.Name
}
}
if defaultGroup == "" {
defaultGroup = cfg.Groups[len(cfg.Groups)-1].Name
}
s, err := service.New(cfg.Service, cfg.Repo, cfg.BaseURL, MilestoneFlag, TokenFlag)
if err != nil {
return err
}
title, prs, err := s.Generate()
if err != nil {
return err
}
PRLoop: // labels in Go, let's get old school
for _, pr := range prs {
if pr.Index < AfterFlag {
continue
}
var label string
for _, lb := range pr.Labels {
if cfg.SkipRegex != nil && cfg.SkipRegex.MatchString(lb.Name) {
continue PRLoop
}
if g, ok := labels[lb.Name]; ok && len(label) == 0 {
label = g
}
}
if len(label) > 0 {
entries[label] = append(entries[label], pr)
} else {
entries[defaultGroup] = append(entries[defaultGroup], pr)
}
}
fmt.Println(title)
for _, g := range cfg.Groups {
if len(entries[g.Name]) == 0 {
continue
}
if DetailsFlag {
fmt.Println("<details><summary>" + g.Name + "</summary>")
fmt.Println()
for _, entry := range entries[g.Name] {
fmt.Printf("* %s (#%d)\n", entry.Title, entry.Index)
}
fmt.Println("</details>")
} else {
fmt.Println("* " + g.Name)
for _, entry := range entries[g.Name] {
fmt.Printf(" * %s (#%d)\n", entry.Title, entry.Index)
}
}
}
return nil
}

View File

@ -1,54 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package main
//go:generate go run changelog.example.go
//go:generate go fmt ./...
import (
"io/ioutil"
"regexp"
"gopkg.in/yaml.v2"
)
var defaultConfig []byte
type Config struct {
Repo string `yaml:"repo"`
Groups []struct {
Name string `yaml:"name"`
Labels []string `yaml:"labels"`
Default bool `yaml:"default"`
} `yaml:"groups"`
SkipLabels string `yaml:"skip-labels"`
SkipRegex *regexp.Regexp `yaml:"-"`
}
func LoadConfig() (*Config, error) {
var err error
var configContent []byte
if len(configPath) == 0 {
configContent = defaultConfig
} else {
configContent, err = ioutil.ReadFile(configPath)
if err != nil {
return nil, err
}
}
var config *Config
if err = yaml.Unmarshal(configContent, &config); err != nil {
return nil, err
}
if len(config.SkipLabels) > 0 {
if config.SkipRegex, err = regexp.Compile(config.SkipLabels); err != nil {
return nil, err
}
}
return config, nil
}

58
config/config.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
import (
"io/ioutil"
"regexp"
"gopkg.in/yaml.v2"
)
var defaultConfig []byte
// Group is a grouping of PRs
type Group struct {
Name string `yaml:"name"`
Labels []string `yaml:"labels"`
Default bool `yaml:"default"`
}
// Config is the changelog settings
type Config struct {
Repo string `yaml:"repo"`
Service string `yaml:"service"`
BaseURL string `yaml:"base-url"`
Groups []Group `yaml:"groups"`
SkipLabels string `yaml:"skip-labels"`
SkipRegex *regexp.Regexp `yaml:"-"`
}
// Load a config from a path, defaulting to changelog.example.yml
func New(configPath string) (*Config, error) {
var err error
var configContent []byte
if len(configPath) == 0 {
configContent = defaultConfig
} else {
configContent, err = ioutil.ReadFile(configPath)
if err != nil {
return nil, err
}
}
var cfg *Config
if err = yaml.Unmarshal(configContent, &cfg); err != nil {
return nil, err
}
if len(cfg.SkipLabels) > 0 {
if cfg.SkipRegex, err = regexp.Compile(cfg.SkipLabels); err != nil {
return nil, err
}
}
return cfg, nil
}

View File

@ -1,9 +1,20 @@
package main
Review

copyright head

copyright head
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package config
func init() {
defaultConfig = []byte(`# The full repository name
repo: go-gitea/gitea
# Service type (gitea or github)
Review

Same concern here

Same concern here
service: github
# Base URL for Gitea instance if using gitea service type (optional)
# Default: https://gitea.com
base-url:
# Changelog groups and which labeled PRs to add to each group
groups:
-

View File

@ -1,70 +0,0 @@
// Copyright 2018 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"fmt"
"log"
"sort"
"github.com/google/go-github/github"
"github.com/urfave/cli/v2"
)
var cmdContributors = &cli.Command{
Name: "contributors",
Usage: "generate contributors list",
Description: "generate contributors list",
Action: runContributors,
}
func runContributors(cmd *cli.Context) error {
config, err := LoadConfig()
if err != nil {
return err
}
client := github.NewClient(nil)
ctx := context.Background()
contributorsMap := make(map[string]bool)
query := fmt.Sprintf(`repo:%s is:merged milestone:"%s"`, config.Repo, milestone)
p := 1
perPage := 100
for {
result, _, err := client.Search.Issues(ctx, query, &github.SearchOptions{
ListOptions: github.ListOptions{
Page: p,
PerPage: perPage,
},
})
p++
if err != nil {
log.Fatal(err.Error())
}
for _, pr := range result.Issues {
contributorsMap[*pr.User.Login] = true
}
if len(result.Issues) != perPage {
break
}
}
contributors := make([]string, 0, len(contributorsMap))
for contributor, _ := range contributorsMap {
contributors = append(contributors, contributor)
}
sort.Strings(contributors)
for _, contributor := range contributors {
fmt.Printf("* [@%s](https://github.com/%s)\n", contributor, contributor)
}
return nil
}

View File

@ -1,103 +0,0 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/google/go-github/github"
"github.com/urfave/cli/v2"
)
var cmdGenerate = &cli.Command{
Name: "generate",
Usage: "generate changelog",
Description: "generate changelog",
Action: runGenerate,
}
func runGenerate(cmd *cli.Context) error {
config, err := LoadConfig()
if err != nil {
return err
}
client := github.NewClient(nil)
ctx := context.Background()
labels := make(map[string]string)
changelogs := make(map[string][]github.Issue)
var defaultGroup string
for _, g := range config.Groups {
changelogs[g.Name] = []github.Issue{}
for _, l := range g.Labels {
labels[l] = g.Name
}
if g.Default {
defaultGroup = g.Name
}
}
if defaultGroup == "" {
defaultGroup = config.Groups[len(config.Groups)-1].Name
}
query := fmt.Sprintf(`repo:%s is:merged milestone:"%s"`, config.Repo, milestone)
p := 1
perPage := 100
for {
result, _, err := client.Search.Issues(ctx, query, &github.SearchOptions{
ListOptions: github.ListOptions{
Page: p,
PerPage: perPage,
},
})
p++
if err != nil {
log.Fatal(err.Error())
}
PRLoop: // labels in Go, let's get old school
for _, pr := range result.Issues {
var label string
for _, lb := range pr.Labels {
if config.SkipRegex != nil && config.SkipRegex.MatchString(lb.GetName()) {
continue PRLoop
}
if g, ok := labels[lb.GetName()]; ok && len(label) == 0 {
label = g
}
}
if len(label) > 0 {
changelogs[label] = append(changelogs[label], pr)
} else {
changelogs[defaultGroup] = append(changelogs[defaultGroup], pr)
}
}
if len(result.Issues) != perPage {
break
}
}
fmt.Printf("## [%s](https://github.com/%s/releases/tag/v%s) - %s\n", milestone, config.Repo, milestone, time.Now().Format("2006-01-02"))
for _, g := range config.Groups {
if len(changelogs[g.Name]) == 0 {
continue
}
fmt.Println("* " + g.Name)
for _, pr := range changelogs[g.Name] {
fmt.Printf(" * %s (#%d)\n", *pr.Title, *pr.Number)
}
}
return nil
}

1
go.mod
View File

@ -3,6 +3,7 @@ module code.gitea.io/changelog
go 1.13
require (
code.gitea.io/sdk/gitea v0.0.0-20200116035226-b24cfd841cda
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
github.com/google/go-github v17.0.0+incompatible
github.com/google/go-querystring v1.0.0 // indirect

7
go.sum
View File

@ -1,7 +1,11 @@
code.gitea.io/sdk/gitea v0.0.0-20200116035226-b24cfd841cda h1:J+qDCjmjcewNcPNfHIex5z726cgv/URXK0MnXHTIo1U=
code.gitea.io/sdk/gitea v0.0.0-20200116035226-b24cfd841cda/go.mod h1:SXOCD/+QP5txLJQ2bPkgHGSQs1YQ4s1ep1ZpI6ItO4A=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
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/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
@ -12,6 +16,9 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k=
github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

35
main.go
View File

@ -4,10 +4,14 @@
Review

copyright head ...

copyright head ...
Review

There is already a copyright here.

There is already a copyright here.
package main
//go:generate go run changelog.example.go
//go:generate go fmt ./...
import (
"fmt"
"os"
"code.gitea.io/changelog/cmd"
"github.com/urfave/cli/v2"
)
@ -16,11 +20,6 @@ const (
Version = "0.2"
)
var (
milestone string
configPath string
)
func main() {
app := &cli.App{
Name: "changelog",
@ -32,18 +31,36 @@ func main() {
Aliases: []string{"m"},
Usage: "Targeted milestone",
Required: true,
Destination: &milestone,
Destination: &cmd.MilestoneFlag,
},
&cli.StringFlag{
Name: "config",
Aliases: []string{"c"},
Usage: "Specify a config file",
Destination: &configPath,
Destination: &cmd.ConfigPathFlag,
},
&cli.StringFlag{
Name: "token",
Aliases: []string{"t"},
Usage: "Access token for private repositories/instances",
Destination: &cmd.TokenFlag,
},
&cli.BoolFlag{
Name: "details",
Aliases: []string{"d"},
Usage: "Generate detail lists instead of long lists",
Destination: &cmd.DetailsFlag,
},
&cli.Int64Flag{
Name: "after",
Aliases: []string{"a"},
Usage: "Only select PRs after a given index (continuing a previous changelog)",
Destination: &cmd.AfterFlag,
},
},
Commands: []*cli.Command{
cmdGenerate,
cmdContributors,
cmd.Generate,
cmd.Contributors,
},
}

138
service/gitea.go Normal file
View File

@ -0,0 +1,138 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package service
import (
"fmt"
"time"
"code.gitea.io/sdk/gitea"
)
// Gitea defines a Gitea service
type Gitea struct {
Milestone string
Token string
BaseURL string
Owner string
Repo string
}
// Generate returns a Gitea changelog
func (ge *Gitea) Generate() (string, []PullRequest, error) {
client := gitea.NewClient(ge.BaseURL, ge.Token)
prs := make([]PullRequest, 0)
milestoneID, err := ge.milestoneID(client)
if err != nil {
return "", nil, err
}
tagURL := fmt.Sprintf("## [%s](%s/%s/%s/pulls?q=&type=all&state=closed&milestone=%d) - %s", ge.Milestone, ge.BaseURL, ge.Owner, ge.Repo, milestoneID, time.Now().Format("2006-01-02"))
p := 1
// https://github.com/go-gitea/gitea/blob/d92781bf941972761177ac9e07441f8893758fd3/models/repo.go#L63
// https://github.com/go-gitea/gitea/blob/e3c3b33ea7a5a223e22688c3f0eb2d3dab9f991c/models/pull_list.go#L104
Review

can you write the reason instead of linking to source code?

can you write the reason instead of linking to source code?
Review

Done.

Done.
// FIXME Gitea has this hard-coded at 40
perPage := 40
for {
results, err := client.ListRepoPullRequests(ge.Owner, ge.Repo, gitea.ListPullRequestsOptions{
Page: p,
State: "closed",
Milestone: milestoneID,
})
if err != nil {
return "", nil, err
}
p++
for _, pr := range results {
if pr != nil && pr.HasMerged {
p := PullRequest{
Title: pr.Title,
Index: pr.Index,
}
labels := make([]Label, len(pr.Labels))
for idx, lbl := range pr.Labels {
labels[idx] = Label{
Name: lbl.Name,
}
}
p.Labels = labels
prs = append(prs, p)
}
}
if len(results) != perPage {
break
}
}
return tagURL, prs, nil
}
// Contributors returns a list of contributors from Gitea
func (ge *Gitea) Contributors() (ContributorList, error) {
client := gitea.NewClient(ge.BaseURL, ge.Token)
contributorsMap := make(map[string]bool)
milestoneID, err := ge.milestoneID(client)
if err != nil {
return nil, err
}
p := 1
perPage := 100
for {
results, err := client.ListRepoPullRequests(ge.Owner, ge.Repo, gitea.ListPullRequestsOptions{
Page: p,
State: "closed",
Milestone: milestoneID,
})
if err != nil {
return nil, err
}
p++
for _, pr := range results {
if pr != nil && pr.HasMerged {
contributorsMap[pr.Poster.UserName] = true
}
}
if len(results) != perPage {
break
}
}
contributors := make(ContributorList, 0, len(contributorsMap))
for contributor, _ := range contributorsMap {
contributors = append(contributors, Contributor{
Name: contributor,
Profile: fmt.Sprintf("%s/%s", ge.BaseURL, contributor),
})
}
return contributors, nil
}
func (ge *Gitea) milestoneID(client *gitea.Client) (int64, error) {
milestones, err := client.ListRepoMilestones(ge.Owner, ge.Repo)
if err != nil {
return 0, err
}
for _, ms := range milestones {
if ms.Title == ge.Milestone {
return ms.ID, nil
}
}
return 0, fmt.Errorf("no milestone found for %s", ge.Milestone)
}

112
service/github.go Normal file
View File

@ -0,0 +1,112 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package service
import (
"context"
"fmt"
"time"
"github.com/google/go-github/github"
)
// GitHub defines a GitHub service
type GitHub struct {
Milestone string
Token string
Repo string
}
// Generate returns a GitHub changelog
func (gh *GitHub) Generate() (string, []PullRequest, error) {
tagURL := fmt.Sprintf("## [%s](https://github.com/%s/releases/tag/v%s) - %s", gh.Milestone, gh.Repo, gh.Milestone, time.Now().Format("2006-01-02"))
client := github.NewClient(nil)
ctx := context.Background()
prs := make([]PullRequest, 0)
query := fmt.Sprintf(`repo:%s is:merged milestone:"%s"`, gh.Repo, gh.Milestone)
p := 1
perPage := 100
for {
result, _, err := client.Search.Issues(ctx, query, &github.SearchOptions{
ListOptions: github.ListOptions{
Page: p,
PerPage: perPage,
},
})
if err != nil {
return "", nil, err
}
p++
for _, pr := range result.Issues {
if pr.IsPullRequest() {
p := PullRequest{
Title: pr.GetTitle(),
Index: int64(pr.GetNumber()),
}
labels := make([]Label, len(pr.Labels))
for idx, lbl := range pr.Labels {
labels[idx] = Label{
Name: lbl.GetName(),
}
}
p.Labels = labels
prs = append(prs, p)
}
}
if len(result.Issues) != perPage {
break
}
}
return tagURL, prs, nil
}
// Contributors returns a list of contributors from GitHub
func (gh *GitHub) Contributors() (ContributorList, error) {
client := github.NewClient(nil)
ctx := context.Background()
contributorsMap := make(map[string]bool)
query := fmt.Sprintf(`repo:%s is:merged milestone:"%s"`, gh.Repo, gh.Milestone)
p := 1
perPage := 100
for {
result, _, err := client.Search.Issues(ctx, query, &github.SearchOptions{
ListOptions: github.ListOptions{
Page: p,
PerPage: perPage,
},
})
if err != nil {
return nil, err
}
p++
for _, pr := range result.Issues {
contributorsMap[pr.GetUser().GetLogin()] = true
}
if len(result.Issues) != perPage {
break
}
}
contributors := make(ContributorList, 0, len(contributorsMap))
for contributor, _ := range contributorsMap {
contributors = append(contributors, Contributor{
Name: contributor,
Profile: fmt.Sprintf("https://github.com/%s", contributor),
})
}
return contributors, nil
}

38
service/github_test.go Normal file
View File

@ -0,0 +1,38 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
Outdated
Review

copyright head

copyright head
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package service
import "testing"
var gh = &GitHub{
Milestone: "1.1.0", // https://github.com/go-gitea/test_repo/milestone/2?closed=1
Repo: "go-gitea/test_repo",
}
func TestGitHubGenerate(t *testing.T) {
_, entries, err := gh.Generate()
if err != nil {
t.Log(err)
t.FailNow()
}
if len(entries) != 1 {
t.Logf("Expected 1 changelog entry, but got %d", len(entries))
t.Fail()
}
}
func TestGitHubContributors(t *testing.T) {
contributors, err := gh.Contributors()
if err != nil {
t.Log(err)
t.FailNow()
}
if len(contributors) != 1 {
t.Logf("Expected 1 contributor, but got %d", len(contributors))
t.Fail()
}
}

80
service/service.go Normal file
View File

@ -0,0 +1,80 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package service
import (
"fmt"
"strings"
)
const defaultGitea = "https://gitea.com"
// Load returns a service from a string
func New(serviceType, repo, baseURL, milestone, token string) (Service, error) {
switch strings.ToLower(serviceType) {
case "github":
return &GitHub{
Milestone: milestone,
Token: token,
Repo: repo,
}, nil
case "gitea":
ownerRepo := strings.Split(repo, "/")
if strings.TrimSpace(baseURL) == "" {
baseURL = defaultGitea
}
return &Gitea{
Milestone: milestone,
Token: token,
BaseURL: baseURL,
Owner: ownerRepo[0],
Repo: ownerRepo[1],
}, nil
default:
return nil, fmt.Errorf("unknown service type %s", serviceType)
}
}
// Service defines how a struct can be a Changelog Service
type Service interface {
Generate() (string, []PullRequest, error)
Contributors() (ContributorList, error)
}
// Label is the minimum information needed for a PR label
type Label struct {
Name string
}
// PullRequest is the minimum information needed to make a changelog entry
type PullRequest struct {
Title string
Index int64
Labels []Label
}
// Contributor is a project contributor
type Contributor struct {
Name string
Profile string
}
// ContributorList is a slice of Contributors that can be sorted
type ContributorList []Contributor
// Len is the length of the ContributorList
func (cl ContributorList) Len() int {
return len(cl)
}
// Less determines whether a Contributor comes before another Contributor
func (cl ContributorList) Less(i, j int) bool {
return cl[i].Name < cl[j].Name
}
// Swap swaps Contributors in a ContributorList
func (cl ContributorList) Swap(i, j int) {
cl[i], cl[j] = cl[j], cl[i]
}

14
service/service_test.go Normal file
View File

@ -0,0 +1,14 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
Outdated
Review

copyright head

copyright head
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package service
import (
"os"
"testing"
)
func TestMain(m *testing.M) {
os.Exit(m.Run())
}