forked from lunny/changelog
240 lines
4.9 KiB
Go
240 lines
4.9 KiB
Go
// 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"
|
|
"errors"
|
|
"fmt"
|
|
log "log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/go-github/v61/github"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
// GitHub defines a GitHub service
|
|
type GitHub struct {
|
|
Milestone string
|
|
GitTag string
|
|
Token string
|
|
Repo string
|
|
Issues bool
|
|
ctx context.Context
|
|
client *github.Client
|
|
rate *github.Rate
|
|
}
|
|
|
|
func (gh *GitHub) setRate(rate *github.Rate) {
|
|
gh.rate = rate
|
|
}
|
|
|
|
func (gh *GitHub) RefreshRate() error {
|
|
rates, _, err := gh.client.RateLimit.Get(gh.ctx)
|
|
if err != nil {
|
|
// if rate limit is not enabled, ignore it
|
|
if strings.Contains(err.Error(), "404") {
|
|
gh.setRate(nil)
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
gh.setRate(rates.GetCore())
|
|
return nil
|
|
}
|
|
|
|
func (gh *GitHub) waitAndPickClient() {
|
|
for gh.rate != nil && gh.rate.Remaining <= 0 {
|
|
timer := time.NewTimer(time.Until(gh.rate.Reset.Time))
|
|
select {
|
|
case <-gh.ctx.Done():
|
|
timer.Stop()
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
|
|
err := gh.RefreshRate()
|
|
if err != nil {
|
|
log.Error("g.getClient().RateLimit.Get: %s", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// OwnerRepo splits owner/repo
|
|
func (gh *GitHub) OwnerRepo() (string, string) {
|
|
parts := strings.Split(gh.Repo, "/")
|
|
if len(parts) < 2 {
|
|
return parts[0], ""
|
|
}
|
|
return parts[0], parts[1]
|
|
}
|
|
|
|
// Generate returns a GitHub changelog
|
|
func (gh *GitHub) Generate(ctx context.Context) (string, []Entry, error) {
|
|
owner, repo := gh.OwnerRepo()
|
|
gh.initClient(ctx)
|
|
|
|
tagURL := fmt.Sprintf("## [%s](https://github.com/%s/releases/tag/v%s) - %s", gh.Milestone, gh.Repo, gh.GitTag, time.Now().Format("2006-01-02"))
|
|
|
|
prs := make([]Entry, 0)
|
|
|
|
milestoneNum, err := gh.milestoneNum(ctx)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
p := 1
|
|
perPage := 100
|
|
for {
|
|
gh.waitAndPickClient()
|
|
result, resp, err := gh.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{
|
|
Milestone: strconv.Itoa(milestoneNum),
|
|
State: "closed",
|
|
ListOptions: github.ListOptions{
|
|
Page: p,
|
|
PerPage: perPage,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
gh.setRate(&resp.Rate)
|
|
p++
|
|
|
|
isPull := !(gh.Issues)
|
|
|
|
for _, pr := range result {
|
|
if pr.IsPullRequest() == isPull {
|
|
p := Entry{
|
|
Title: CleanTitle(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) != perPage {
|
|
break
|
|
}
|
|
}
|
|
|
|
return tagURL, prs, nil
|
|
}
|
|
|
|
// Contributors returns a list of contributors from GitHub
|
|
func (gh *GitHub) Contributors() (ContributorList, error) {
|
|
ctx := context.Background()
|
|
owner, repo := gh.OwnerRepo()
|
|
gh.initClient(ctx)
|
|
|
|
contributorsMap := make(map[string]bool)
|
|
milestoneNum, err := gh.milestoneNum(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p := 1
|
|
perPage := 100
|
|
for {
|
|
gh.waitAndPickClient()
|
|
result, resp, err := gh.client.Issues.ListByRepo(ctx, owner, repo, &github.IssueListByRepoOptions{
|
|
Milestone: strconv.Itoa(milestoneNum),
|
|
State: "closed",
|
|
ListOptions: github.ListOptions{
|
|
Page: p,
|
|
PerPage: perPage,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
gh.setRate(&resp.Rate)
|
|
p++
|
|
|
|
for _, pr := range result {
|
|
contributorsMap[pr.GetUser().GetLogin()] = true
|
|
}
|
|
|
|
if len(result) != 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
|
|
}
|
|
|
|
func (gh *GitHub) initClient(ctx context.Context) {
|
|
token := gh.Token
|
|
if envToken, ok := os.LookupEnv("CHANGELOG_GITHUB_TOKEN"); ok && token == "" {
|
|
token = envToken
|
|
}
|
|
cl := http.DefaultClient
|
|
if token != "" {
|
|
ts := oauth2.StaticTokenSource(
|
|
&oauth2.Token{AccessToken: token},
|
|
)
|
|
cl = oauth2.NewClient(ctx, ts)
|
|
}
|
|
|
|
gh.client = github.NewClient(cl)
|
|
gh.ctx = ctx
|
|
}
|
|
|
|
func (gh *GitHub) milestoneNum(ctx context.Context) (int, error) {
|
|
owner, repo := gh.OwnerRepo()
|
|
p := 1
|
|
perPage := 100
|
|
for {
|
|
gh.waitAndPickClient()
|
|
milestones, resp, err := gh.client.Issues.ListMilestones(ctx, owner, repo, &github.MilestoneListOptions{
|
|
State: "all",
|
|
ListOptions: github.ListOptions{
|
|
Page: p,
|
|
PerPage: perPage,
|
|
},
|
|
Sort: "due_on",
|
|
Direction: "desc",
|
|
})
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
gh.setRate(&resp.Rate)
|
|
p++
|
|
|
|
for _, milestone := range milestones {
|
|
if strings.EqualFold(milestone.GetTitle(), gh.Milestone) {
|
|
return milestone.GetNumber(), nil
|
|
}
|
|
}
|
|
|
|
if len(milestones) != perPage {
|
|
break
|
|
}
|
|
}
|
|
|
|
return 0, errors.New("no milestone found")
|
|
}
|