gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/tools/github/reviver/github.go (about)

     1  // Copyright 2019 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package reviver
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/google/go-github/github"
    25  )
    26  
    27  // GitHubBugger implements Bugger interface for github issues.
    28  type GitHubBugger struct {
    29  	owner  string
    30  	repo   string
    31  	dryRun bool
    32  
    33  	client *github.Client
    34  	issues map[int]*github.Issue
    35  }
    36  
    37  // NewGitHubBugger creates a new GitHubBugger.
    38  func NewGitHubBugger(client *github.Client, owner, repo string, dryRun bool) (*GitHubBugger, error) {
    39  	b := &GitHubBugger{
    40  		owner:  owner,
    41  		repo:   repo,
    42  		dryRun: dryRun,
    43  		issues: map[int]*github.Issue{},
    44  		client: client,
    45  	}
    46  	if err := b.load(); err != nil {
    47  		return nil, err
    48  	}
    49  	return b, nil
    50  }
    51  
    52  func (b *GitHubBugger) load() error {
    53  	err := processAllPages(func(listOpts github.ListOptions) (*github.Response, error) {
    54  		opts := &github.IssueListByRepoOptions{State: "open", ListOptions: listOpts}
    55  		tmps, resp, err := b.client.Issues.ListByRepo(context.Background(), b.owner, b.repo, opts)
    56  		if err != nil {
    57  			return resp, err
    58  		}
    59  		for _, issue := range tmps {
    60  			b.issues[issue.GetNumber()] = issue
    61  		}
    62  		return resp, nil
    63  	})
    64  	if err != nil {
    65  		return err
    66  	}
    67  
    68  	fmt.Printf("Loaded %d issues from github.com/%s/%s\n", len(b.issues), b.owner, b.repo)
    69  	return nil
    70  }
    71  
    72  // Activate implements Bugger.Activate.
    73  func (b *GitHubBugger) Activate(todo *Todo) (bool, error) {
    74  	id, err := parseIssueNo(todo.Issue)
    75  	if err != nil {
    76  		return true, err
    77  	}
    78  	if id <= 0 {
    79  		return false, nil
    80  	}
    81  
    82  	// Check against active issues cache.
    83  	if _, ok := b.issues[id]; ok {
    84  		fmt.Printf("%q is active: OK\n", todo.Issue)
    85  		return true, nil
    86  	}
    87  
    88  	fmt.Printf("%q is not active: reopening issue %d\n", todo.Issue, id)
    89  
    90  	// Format comment with TODO locations and search link.
    91  	comment := strings.Builder{}
    92  	fmt.Fprintln(&comment, "There are TODOs still referencing this issue:")
    93  	for _, l := range todo.Locations {
    94  		fmt.Fprintf(&comment,
    95  			"1. [%s:%d](https://github.com/%s/%s/blob/HEAD/%s#L%d): %s\n",
    96  			l.File, l.Line, b.owner, b.repo, l.File, l.Line, l.Comment)
    97  	}
    98  	fmt.Fprintf(&comment,
    99  		"\n\nSearch [TODO](https://github.com/%s/%s/search?q=%%22%s%%22)", b.owner, b.repo, todo.Issue)
   100  
   101  	if b.dryRun {
   102  		fmt.Printf("[dry-run: skipping change to issue %d]\n%s\n=======================\n", id, comment.String())
   103  		return true, nil
   104  	}
   105  
   106  	ctx := context.Background()
   107  	req := &github.IssueRequest{State: github.String("open")}
   108  	_, _, err = b.client.Issues.Edit(ctx, b.owner, b.repo, id, req)
   109  	if err != nil {
   110  		return true, fmt.Errorf("failed to reactivate issue %d: %v", id, err)
   111  	}
   112  
   113  	_, _, err = b.client.Issues.AddLabelsToIssue(ctx, b.owner, b.repo, id, []string{"revived"})
   114  	if err != nil {
   115  		return true, fmt.Errorf("failed to set label on issue %d: %v", id, err)
   116  	}
   117  
   118  	cmt := &github.IssueComment{
   119  		Body:      github.String(comment.String()),
   120  		Reactions: &github.Reactions{Confused: github.Int(1)},
   121  	}
   122  	if _, _, err := b.client.Issues.CreateComment(ctx, b.owner, b.repo, id, cmt); err != nil {
   123  		return true, fmt.Errorf("failed to add comment to issue %d: %v", id, err)
   124  	}
   125  
   126  	return true, nil
   127  }
   128  
   129  var issuePrefixes = []string{
   130  	"gvisor.dev/issue/",
   131  	"gvisor.dev/issues/",
   132  }
   133  
   134  // parseIssueNo parses the issue number out of the issue url.
   135  //
   136  // 0 is returned if url does not correspond to an issue.
   137  func parseIssueNo(url string) (int, error) {
   138  	// First check if I can handle the TODO.
   139  	var idStr string
   140  	for _, p := range issuePrefixes {
   141  		if str := strings.TrimPrefix(url, p); len(str) < len(url) {
   142  			idStr = str
   143  			break
   144  		}
   145  	}
   146  	if len(idStr) == 0 {
   147  		return 0, nil
   148  	}
   149  
   150  	id, err := strconv.ParseInt(strings.TrimRight(idStr, "/"), 10, 64)
   151  	if err != nil {
   152  		return 0, err
   153  	}
   154  	return int(id), nil
   155  }
   156  
   157  func processAllPages(fn func(github.ListOptions) (*github.Response, error)) error {
   158  	opts := github.ListOptions{PerPage: 1000}
   159  	for {
   160  		resp, err := fn(opts)
   161  		if err != nil {
   162  			if rateErr, ok := err.(*github.RateLimitError); ok {
   163  				duration := rateErr.Rate.Reset.Sub(time.Now())
   164  				if duration > 5*time.Minute {
   165  					return fmt.Errorf("Rate limited for too long: %v", duration)
   166  				}
   167  				fmt.Printf("Rate limited, sleeping for: %v\n", duration)
   168  				time.Sleep(duration)
   169  				continue
   170  			}
   171  			return err
   172  		}
   173  		if resp.NextPage == 0 {
   174  			return nil
   175  		}
   176  		opts.Page = resp.NextPage
   177  	}
   178  }