// 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") }