|
| 1 | +package github |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "fmt" |
| 6 | + "time" |
| 7 | + |
| 8 | + "github.com/grafana/github-datasource/pkg/models" |
| 9 | + "github.com/grafana/grafana-plugin-sdk-go/data" |
| 10 | + "github.com/pkg/errors" |
| 11 | + "github.com/shurcooL/githubv4" |
| 12 | +) |
| 13 | + |
| 14 | +// QueryListPullRequestReviews lists all pull request reviews in a repository |
| 15 | +// |
| 16 | +// { |
| 17 | +// search(query: "is:pr repo:grafana/grafana merged:2020-08-19..*", type: ISSUE, first: 100) { |
| 18 | +// nodes { |
| 19 | +// ... on PullRequest { |
| 20 | +// Number |
| 21 | +// Title |
| 22 | +// URL |
| 23 | +// State |
| 24 | +// Author |
| 25 | +// Repository |
| 26 | +// reviews(first: 100) { |
| 27 | +// createdAt |
| 28 | +// updatedAt |
| 29 | +// state |
| 30 | +// url |
| 31 | +// author { |
| 32 | +// id |
| 33 | +// login |
| 34 | +// name |
| 35 | +// company |
| 36 | +// email |
| 37 | +// url |
| 38 | +// } |
| 39 | +// comments(first: 0) { |
| 40 | +// totalCount |
| 41 | +// } |
| 42 | +// } |
| 43 | +// } |
| 44 | +// } |
| 45 | +// } |
| 46 | +// } |
| 47 | +type QueryListPullRequestReviews struct { |
| 48 | + Search struct { |
| 49 | + Nodes []struct { |
| 50 | + PullRequest struct { |
| 51 | + Number int64 |
| 52 | + Title string |
| 53 | + URL string |
| 54 | + State githubv4.PullRequestState |
| 55 | + Author Author |
| 56 | + Repository Repository |
| 57 | + Reviews struct { |
| 58 | + Nodes []struct { |
| 59 | + Review struct { |
| 60 | + CreatedAt githubv4.DateTime |
| 61 | + UpdatedAt githubv4.DateTime |
| 62 | + URL string |
| 63 | + Author Author |
| 64 | + State githubv4.PullRequestReviewState |
| 65 | + Comments struct { |
| 66 | + TotalCount int64 |
| 67 | + } `graphql:"comments(first: 0)"` |
| 68 | + } `graphql:"... on PullRequestReview"` |
| 69 | + } |
| 70 | + PageInfo models.PageInfo |
| 71 | + } `graphql:"reviews(first: 100, after: $reviewCursor)"` |
| 72 | + } `graphql:"... on PullRequest"` |
| 73 | + } |
| 74 | + PageInfo models.PageInfo |
| 75 | + } `graphql:"search(query: $query, type: ISSUE, first: 100, after: $prCursor)"` |
| 76 | +} |
| 77 | + |
| 78 | +type Author struct { |
| 79 | + User models.User `graphql:"... on User"` |
| 80 | +} |
| 81 | + |
| 82 | +type Review struct { |
| 83 | + CreatedAt githubv4.DateTime |
| 84 | + UpdatedAt githubv4.DateTime |
| 85 | + URL string |
| 86 | + Author Author |
| 87 | + State githubv4.PullRequestReviewState |
| 88 | + CommentsCount int64 |
| 89 | +} |
| 90 | + |
| 91 | +type PullRequestWithReviews struct { |
| 92 | + Number int64 |
| 93 | + Title string |
| 94 | + State githubv4.PullRequestState |
| 95 | + URL string |
| 96 | + Author Author |
| 97 | + Repository Repository |
| 98 | + Reviews []Review |
| 99 | +} |
| 100 | + |
| 101 | +// PullRequestReviews is a list of GitHub Pull Request Reviews |
| 102 | +type PullRequestReviews []PullRequestWithReviews |
| 103 | + |
| 104 | +// Frames coverts the list of Pull Request Reviews to a Grafana DataFrame |
| 105 | +func (prs PullRequestReviews) Frames() data.Frames { |
| 106 | + frame := data.NewFrame( |
| 107 | + "pull_request_reviews", |
| 108 | + data.NewField("pull_request_number", nil, []int64{}), |
| 109 | + data.NewField("pull_request_title", nil, []string{}), |
| 110 | + data.NewField("pull_request_state", nil, []string{}), |
| 111 | + data.NewField("pull_request_url", nil, []string{}), |
| 112 | + data.NewField("pull_request_author_name", nil, []string{}), |
| 113 | + data.NewField("pull_request_author_login", nil, []string{}), |
| 114 | + data.NewField("pull_request_author_email", nil, []string{}), |
| 115 | + data.NewField("pull_request_author_company", nil, []string{}), |
| 116 | + data.NewField("repository", nil, []string{}), |
| 117 | + data.NewField("review_author_name", nil, []string{}), |
| 118 | + data.NewField("review_author_login", nil, []string{}), |
| 119 | + data.NewField("review_author_email", nil, []string{}), |
| 120 | + data.NewField("review_author_company", nil, []string{}), |
| 121 | + data.NewField("review_url", nil, []string{}), |
| 122 | + data.NewField("review_state", nil, []string{}), |
| 123 | + data.NewField("review_comment_count", nil, []int64{}), |
| 124 | + data.NewField("review_updated_at", nil, []time.Time{}), |
| 125 | + data.NewField("review_created_at", nil, []time.Time{}), |
| 126 | + ) |
| 127 | + |
| 128 | + for _, pr := range prs { |
| 129 | + for _, review := range pr.Reviews { |
| 130 | + frame.AppendRow( |
| 131 | + pr.Number, |
| 132 | + pr.Title, |
| 133 | + string(pr.State), |
| 134 | + pr.URL, |
| 135 | + pr.Author.User.Name, |
| 136 | + pr.Author.User.Login, |
| 137 | + pr.Author.User.Email, |
| 138 | + pr.Author.User.Company, |
| 139 | + pr.Repository.NameWithOwner, |
| 140 | + review.Author.User.Name, |
| 141 | + review.Author.User.Login, |
| 142 | + review.Author.User.Email, |
| 143 | + review.Author.User.Company, |
| 144 | + review.URL, |
| 145 | + string(review.State), |
| 146 | + review.CommentsCount, |
| 147 | + review.UpdatedAt.Time, |
| 148 | + review.CreatedAt.Time, |
| 149 | + ) |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + return data.Frames{frame} |
| 154 | +} |
| 155 | + |
| 156 | +// GetAllPullRequestReviews uses the graphql search endpoint API to search all pull requests in the repository |
| 157 | +// and all reviews for those pull requests. |
| 158 | +func GetAllPullRequestReviews(ctx context.Context, client models.Client, opts models.ListPullRequestsOptions) (PullRequestReviews, error) { |
| 159 | + var ( |
| 160 | + variables = map[string]interface{}{ |
| 161 | + "prCursor": (*githubv4.String)(nil), |
| 162 | + "reviewCursor": (*githubv4.String)(nil), |
| 163 | + "query": githubv4.String(buildQuery(opts)), |
| 164 | + } |
| 165 | + |
| 166 | + pullRequestReviews = PullRequestReviews{} |
| 167 | + ) |
| 168 | + |
| 169 | + for { |
| 170 | + q := &QueryListPullRequestReviews{} |
| 171 | + if err := client.Query(ctx, q, variables); err != nil { |
| 172 | + return nil, errors.WithStack(err) |
| 173 | + } |
| 174 | + |
| 175 | + prs := make([]PullRequestWithReviews, len(q.Search.Nodes)) |
| 176 | + |
| 177 | + for i, prNode := range q.Search.Nodes { |
| 178 | + pr := prNode.PullRequest |
| 179 | + |
| 180 | + prs[i] = PullRequestWithReviews{ |
| 181 | + Number: pr.Number, |
| 182 | + Title: pr.Title, |
| 183 | + State: pr.State, |
| 184 | + URL: pr.URL, |
| 185 | + Author: pr.Author, |
| 186 | + Repository: pr.Repository, |
| 187 | + } |
| 188 | + |
| 189 | + for { |
| 190 | + for _, reviewNode := range pr.Reviews.Nodes { |
| 191 | + review := reviewNode.Review |
| 192 | + |
| 193 | + prs[i].Reviews = append(prs[i].Reviews, Review{ |
| 194 | + CreatedAt: review.CreatedAt, |
| 195 | + UpdatedAt: review.UpdatedAt, |
| 196 | + URL: review.URL, |
| 197 | + Author: review.Author, |
| 198 | + State: review.State, |
| 199 | + CommentsCount: review.Comments.TotalCount, |
| 200 | + }) |
| 201 | + } |
| 202 | + |
| 203 | + if !pr.Reviews.PageInfo.HasNextPage { |
| 204 | + variables["reviewCursor"] = (*githubv4.String)(nil) |
| 205 | + break |
| 206 | + } |
| 207 | + |
| 208 | + variables["reviewCursor"] = pr.Reviews.PageInfo.EndCursor |
| 209 | + if err := client.Query(ctx, q, variables); err != nil { |
| 210 | + return nil, errors.WithStack(err) |
| 211 | + } |
| 212 | + } |
| 213 | + } |
| 214 | + |
| 215 | + pullRequestReviews = append(pullRequestReviews, prs...) |
| 216 | + |
| 217 | + if !q.Search.PageInfo.HasNextPage { |
| 218 | + break |
| 219 | + } |
| 220 | + variables["prCursor"] = q.Search.PageInfo.EndCursor |
| 221 | + } |
| 222 | + |
| 223 | + return pullRequestReviews, nil |
| 224 | +} |
| 225 | + |
| 226 | +// GetPullRequestReviewsInRange uses the graphql search endpoint API to find pull request reviews in the given time range. |
| 227 | +func GetPullRequestReviewsInRange(ctx context.Context, client models.Client, opts models.ListPullRequestsOptions, from time.Time, to time.Time) (PullRequestReviews, error) { |
| 228 | + var q string |
| 229 | + |
| 230 | + if opts.TimeField != models.PullRequestNone { |
| 231 | + q = fmt.Sprintf("%s:%s..%s", opts.TimeField.String(), from.Format(time.RFC3339), to.Format(time.RFC3339)) |
| 232 | + } |
| 233 | + |
| 234 | + if opts.Query != nil { |
| 235 | + q = fmt.Sprintf("%s %s", *opts.Query, q) |
| 236 | + } |
| 237 | + |
| 238 | + return GetAllPullRequestReviews(ctx, client, models.ListPullRequestsOptions{ |
| 239 | + Repository: opts.Repository, |
| 240 | + Owner: opts.Owner, |
| 241 | + TimeField: opts.TimeField, |
| 242 | + Query: &q, |
| 243 | + }) |
| 244 | +} |
0 commit comments