2020-01-08 23:40:00 +00:00
|
|
|
// Copyright 2019 The gVisor Authors.
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
2020-09-29 20:14:52 +00:00
|
|
|
package reviver
|
2020-01-08 23:40:00 +00:00
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/google/go-github/github"
|
|
|
|
)
|
|
|
|
|
2020-09-29 20:14:52 +00:00
|
|
|
// GitHubBugger implements Bugger interface for github issues.
|
|
|
|
type GitHubBugger struct {
|
2020-01-08 23:40:00 +00:00
|
|
|
owner string
|
|
|
|
repo string
|
|
|
|
dryRun bool
|
|
|
|
|
|
|
|
client *github.Client
|
|
|
|
issues map[int]*github.Issue
|
|
|
|
}
|
|
|
|
|
2020-09-29 20:14:52 +00:00
|
|
|
// NewGitHubBugger creates a new GitHubBugger.
|
|
|
|
func NewGitHubBugger(client *github.Client, owner, repo string, dryRun bool) (*GitHubBugger, error) {
|
|
|
|
b := &GitHubBugger{
|
2020-01-08 23:40:00 +00:00
|
|
|
owner: owner,
|
|
|
|
repo: repo,
|
|
|
|
dryRun: dryRun,
|
|
|
|
issues: map[int]*github.Issue{},
|
2020-09-29 20:14:52 +00:00
|
|
|
client: client,
|
2020-01-08 23:40:00 +00:00
|
|
|
}
|
2020-09-29 20:14:52 +00:00
|
|
|
if err := b.load(); err != nil {
|
2020-01-08 23:40:00 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
2020-09-29 20:14:52 +00:00
|
|
|
func (b *GitHubBugger) load() error {
|
2020-01-08 23:40:00 +00:00
|
|
|
err := processAllPages(func(listOpts github.ListOptions) (*github.Response, error) {
|
|
|
|
opts := &github.IssueListByRepoOptions{State: "open", ListOptions: listOpts}
|
2020-09-29 20:14:52 +00:00
|
|
|
tmps, resp, err := b.client.Issues.ListByRepo(context.Background(), b.owner, b.repo, opts)
|
2020-01-08 23:40:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return resp, err
|
|
|
|
}
|
|
|
|
for _, issue := range tmps {
|
|
|
|
b.issues[issue.GetNumber()] = issue
|
|
|
|
}
|
|
|
|
return resp, nil
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Loaded %d issues from github.com/%s/%s\n", len(b.issues), b.owner, b.repo)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-09-29 20:14:52 +00:00
|
|
|
// Activate implements Bugger.Activate.
|
|
|
|
func (b *GitHubBugger) Activate(todo *Todo) (bool, error) {
|
2020-08-14 23:21:29 +00:00
|
|
|
id, err := parseIssueNo(todo.Issue)
|
2020-01-08 23:40:00 +00:00
|
|
|
if err != nil {
|
|
|
|
return true, err
|
|
|
|
}
|
2020-08-14 23:21:29 +00:00
|
|
|
if id <= 0 {
|
|
|
|
return false, nil
|
|
|
|
}
|
2020-01-08 23:40:00 +00:00
|
|
|
|
|
|
|
// Check against active issues cache.
|
|
|
|
if _, ok := b.issues[id]; ok {
|
|
|
|
fmt.Printf("%q is active: OK\n", todo.Issue)
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("%q is not active: reopening issue %d\n", todo.Issue, id)
|
|
|
|
|
|
|
|
// Format comment with TODO locations and search link.
|
|
|
|
comment := strings.Builder{}
|
|
|
|
fmt.Fprintln(&comment, "There are TODOs still referencing this issue:")
|
|
|
|
for _, l := range todo.Locations {
|
|
|
|
fmt.Fprintf(&comment,
|
2021-05-13 01:06:38 +00:00
|
|
|
"1. [%s:%d](https://github.com/%s/%s/blob/HEAD/%s#L%d): %s\n",
|
2020-01-08 23:40:00 +00:00
|
|
|
l.File, l.Line, b.owner, b.repo, l.File, l.Line, l.Comment)
|
|
|
|
}
|
|
|
|
fmt.Fprintf(&comment,
|
2020-08-14 23:21:29 +00:00
|
|
|
"\n\nSearch [TODO](https://github.com/%s/%s/search?q=%%22%s%%22)", b.owner, b.repo, todo.Issue)
|
2020-01-08 23:40:00 +00:00
|
|
|
|
|
|
|
if b.dryRun {
|
|
|
|
fmt.Printf("[dry-run: skipping change to issue %d]\n%s\n=======================\n", id, comment.String())
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
req := &github.IssueRequest{State: github.String("open")}
|
|
|
|
_, _, err = b.client.Issues.Edit(ctx, b.owner, b.repo, id, req)
|
|
|
|
if err != nil {
|
|
|
|
return true, fmt.Errorf("failed to reactivate issue %d: %v", id, err)
|
|
|
|
}
|
|
|
|
|
2021-05-11 00:27:08 +00:00
|
|
|
_, _, err = b.client.Issues.AddLabelsToIssue(ctx, b.owner, b.repo, id, []string{"revived"})
|
|
|
|
if err != nil {
|
|
|
|
return true, fmt.Errorf("failed to set label on issue %d: %v", id, err)
|
|
|
|
}
|
|
|
|
|
2020-01-08 23:40:00 +00:00
|
|
|
cmt := &github.IssueComment{
|
|
|
|
Body: github.String(comment.String()),
|
|
|
|
Reactions: &github.Reactions{Confused: github.Int(1)},
|
|
|
|
}
|
|
|
|
if _, _, err := b.client.Issues.CreateComment(ctx, b.owner, b.repo, id, cmt); err != nil {
|
|
|
|
return true, fmt.Errorf("failed to add comment to issue %d: %v", id, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2020-10-24 02:24:51 +00:00
|
|
|
var issuePrefixes = []string{
|
|
|
|
"gvisor.dev/issue/",
|
|
|
|
"gvisor.dev/issues/",
|
|
|
|
}
|
|
|
|
|
2020-08-14 23:21:29 +00:00
|
|
|
// parseIssueNo parses the issue number out of the issue url.
|
2020-10-24 02:24:51 +00:00
|
|
|
//
|
|
|
|
// 0 is returned if url does not correspond to an issue.
|
2020-08-14 23:21:29 +00:00
|
|
|
func parseIssueNo(url string) (int, error) {
|
|
|
|
// First check if I can handle the TODO.
|
2020-10-24 02:24:51 +00:00
|
|
|
var idStr string
|
|
|
|
for _, p := range issuePrefixes {
|
|
|
|
if str := strings.TrimPrefix(url, p); len(str) < len(url) {
|
|
|
|
idStr = str
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(idStr) == 0 {
|
2020-08-14 23:21:29 +00:00
|
|
|
return 0, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
id, err := strconv.ParseInt(strings.TrimRight(idStr, "/"), 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return 0, err
|
|
|
|
}
|
|
|
|
return int(id), nil
|
|
|
|
}
|
|
|
|
|
2020-01-08 23:40:00 +00:00
|
|
|
func processAllPages(fn func(github.ListOptions) (*github.Response, error)) error {
|
|
|
|
opts := github.ListOptions{PerPage: 1000}
|
|
|
|
for {
|
|
|
|
resp, err := fn(opts)
|
|
|
|
if err != nil {
|
|
|
|
if rateErr, ok := err.(*github.RateLimitError); ok {
|
|
|
|
duration := rateErr.Rate.Reset.Sub(time.Now())
|
|
|
|
if duration > 5*time.Minute {
|
|
|
|
return fmt.Errorf("Rate limited for too long: %v", duration)
|
|
|
|
}
|
|
|
|
fmt.Printf("Rate limited, sleeping for: %v\n", duration)
|
|
|
|
time.Sleep(duration)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if resp.NextPage == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
opts.Page = resp.NextPage
|
|
|
|
}
|
|
|
|
}
|