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 }