diff --git a/changelog.example.go b/changelog.example.go index a896604..2db8d32 100644 --- a/changelog.example.go +++ b/changelog.example.go @@ -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` + "`" + `) diff --git a/changelog.example.yml b/changelog.example.yml index 48164d1..d092155 100644 --- a/changelog.example.yml +++ b/changelog.example.yml @@ -1,6 +1,13 @@ # The full repository name 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 +base-url: + # Changelog groups and which labeled PRs to add to each group groups: - diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..12642da --- /dev/null +++ b/cmd/cmd.go @@ -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 +) diff --git a/cmd/contributors.go b/cmd/contributors.go new file mode 100644 index 0000000..0787bb5 --- /dev/null +++ b/cmd/contributors.go @@ -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 +} diff --git a/cmd/generate.go b/cmd/generate.go new file mode 100644 index 0000000..bd89e42 --- /dev/null +++ b/cmd/generate.go @@ -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", + 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("
" + g.Name + "") + fmt.Println() + for _, entry := range entries[g.Name] { + fmt.Printf("* %s (#%d)\n", entry.Title, entry.Index) + } + fmt.Println("
") + } else { + fmt.Println("* " + g.Name) + for _, entry := range entries[g.Name] { + fmt.Printf(" * %s (#%d)\n", entry.Title, entry.Index) + } + } + } + + return nil +} diff --git a/config.go b/config.go deleted file mode 100644 index ea56b54..0000000 --- a/config.go +++ /dev/null @@ -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 -} diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..e265b03 --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config_default.go b/config/config_default.go similarity index 71% rename from config_default.go rename to config/config_default.go index 20a19be..7113f98 100644 --- a/config_default.go +++ b/config/config_default.go @@ -1,9 +1,20 @@ -package main +// 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) +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: - diff --git a/contributors.go b/contributors.go deleted file mode 100644 index 65aeace..0000000 --- a/contributors.go +++ /dev/null @@ -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 -} diff --git a/generate.go b/generate.go deleted file mode 100644 index 74a97df..0000000 --- a/generate.go +++ /dev/null @@ -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 -} diff --git a/go.mod b/go.mod index a967178..40114aa 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 9832c0a..6c67ad7 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index dd476bc..f23b659 100644 --- a/main.go +++ b/main.go @@ -4,10 +4,14 @@ 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, }, } diff --git a/service/gitea.go b/service/gitea.go new file mode 100644 index 0000000..cfcb642 --- /dev/null +++ b/service/gitea.go @@ -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 + // 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) +} diff --git a/service/github.go b/service/github.go new file mode 100644 index 0000000..bea8f09 --- /dev/null +++ b/service/github.go @@ -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 +} diff --git a/service/github_test.go b/service/github_test.go new file mode 100644 index 0000000..562216d --- /dev/null +++ b/service/github_test.go @@ -0,0 +1,38 @@ +// 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 "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() + } +} diff --git a/service/service.go b/service/service.go new file mode 100644 index 0000000..9fa7822 --- /dev/null +++ b/service/service.go @@ -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] +} diff --git a/service/service_test.go b/service/service_test.go new file mode 100644 index 0000000..045e70f --- /dev/null +++ b/service/service_test.go @@ -0,0 +1,14 @@ +// 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 ( + "os" + "testing" +) + +func TestMain(m *testing.M) { + os.Exit(m.Run()) +}