forked from drone/go-scm
-
Notifications
You must be signed in to change notification settings - Fork 86
/
main.go
209 lines (190 loc) · 5.62 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
package main
import (
"context"
"fmt"
"os"
"strings"
"time"
githubql "github.com/shurcooL/githubv4"
"github.com/sirupsen/logrus"
"github.com/jenkins-x/go-scm/scm"
"github.com/jenkins-x/go-scm/scm/factory"
"github.com/jenkins-x/go-scm/scm/factory/examples/helpers"
)
var (
// searchTimeFormat is a time.Time format string for ISO8601 which is the
// format that GitHub requires for times specified as part of a search query.
searchTimeFormat = "2006-01-02T15:04:05Z"
// FoundingYear is the year GitHub was founded. This is just used so that
// we can lower bound dates related to PRs and issues.
foundingYear, _ = time.Parse(searchTimeFormat, "2007-01-01T00:00:00Z")
)
// PullRequest holds graphql data about a PR, including its commits and their contexts.
type PullRequest struct {
Number githubql.Int
Author struct {
Login githubql.String
}
BaseRef struct {
Name githubql.String
Prefix githubql.String
}
HeadRefName githubql.String `graphql:"headRefName"`
HeadRefOID githubql.String `graphql:"headRefOid"`
Mergeable githubql.MergeableState
Repository struct {
Name githubql.String
NameWithOwner githubql.String
Owner struct {
Login githubql.String
}
}
Commits struct {
Nodes []struct {
Commit Commit
}
// Request the 'last' 4 commits hoping that one of them is the logically 'last'
// commit with OID matching HeadRefOID. If we don't find it we have to use an
// additional API token. (see the 'headContexts' func for details)
// We can't raise this too much or we could hit the limit of 50,000 nodes
// per query: https://developer.github.com/v4/guides/resource-limitations/#node-limit
} `graphql:"commits(last: 4)"`
Labels struct {
Nodes []struct {
Name githubql.String
}
} `graphql:"labels(first: 100)"`
Milestone *struct {
Title githubql.String
}
Body githubql.String
Title githubql.String
UpdatedAt githubql.DateTime
}
// Commit holds graphql data about commits and which contexts they have
type Commit struct {
Status struct {
Contexts []Context
}
OID githubql.String `graphql:"oid"`
}
// Context holds graphql response data for github contexts.
type Context struct {
Context githubql.String
Description githubql.String
State githubql.StatusState
}
// PRNode is used to represent a PR
type PRNode struct {
PullRequest PullRequest `graphql:"... on PullRequest"`
}
type searchQuery struct {
RateLimit struct {
Cost githubql.Int
Remaining githubql.Int
}
Search struct {
PageInfo struct {
HasNextPage githubql.Boolean
EndCursor githubql.String
}
Nodes []PRNode
} `graphql:"search(type: ISSUE, first: 100, after: $searchCursor, query: $query)"`
}
func main() {
client, err := factory.NewClientFromEnvironment()
if err != nil {
helpers.Fail(err)
return
}
args := os.Args
if len(args) < 2 {
fmt.Printf("usage: queryString")
return
}
query := args[1]
fmt.Printf("searching issues and pull requests via GraphQL query %s\n", query)
graphql := client.GraphQL
if graphql == nil {
helpers.Fail(fmt.Errorf("No GraphQL support for driver %s", client.Driver.String()))
return
}
results, err := search(client, logrus.WithField("query", query), query, time.Time{}, time.Now())
if err != nil {
helpers.Fail(err)
return
}
fmt.Printf("Found %d results\n", len(results))
for _, r := range results {
commits := []string{}
for _, commit := range r.Commits.Nodes {
commits = append(commits, string(commit.Commit.OID))
}
fmt.Printf("PR %s #%d title: %s commits: %s\n", string(r.Repository.NameWithOwner), r.Number, string(r.Title), strings.Join(commits, ", "))
}
}
func datedQuery(q string, start, end time.Time) string {
return fmt.Sprintf("%s %s", q, dateToken(start, end))
}
func floor(t time.Time) time.Time {
if t.Before(foundingYear) {
return foundingYear
}
return t
}
func search(client *scm.Client, log *logrus.Entry, q string, start, end time.Time) ([]PullRequest, error) {
start = floor(start)
end = floor(end)
log = log.WithFields(logrus.Fields{
"query": q,
"start": start.String(),
"end": end.String(),
})
requestStart := time.Now()
var cursor *githubql.String
vars := map[string]interface{}{
"query": githubql.String(datedQuery(q, start, end)),
"searchCursor": cursor,
}
var totalCost, remaining int
var ret []PullRequest
var sq searchQuery
ctx := context.Background()
for {
log.Debug("Sending query")
if err := client.GraphQL.Query(ctx, &sq, vars); err != nil {
if cursor != nil {
err = fmt.Errorf("cursor: %q, err: %v", *cursor, err)
}
return ret, err
}
totalCost += int(sq.RateLimit.Cost)
remaining = int(sq.RateLimit.Remaining)
for _, n := range sq.Search.Nodes {
ret = append(ret, n.PullRequest)
}
if !sq.Search.PageInfo.HasNextPage {
break
}
cursor = &sq.Search.PageInfo.EndCursor
vars["searchCursor"] = cursor
log = log.WithField("searchCursor", *cursor)
}
log.WithField("duration", time.Since(requestStart).String()).Debugf("GraphQL returned %d PRs and cost %d point(s). %d remaining.", len(ret), totalCost, remaining)
return ret, nil
}
// dateToken generates a GitHub search query token for the specified date range.
// See: https://help.github.com/articles/understanding-the-search-syntax/#query-for-dates
func dateToken(start, end time.Time) string {
// GitHub's GraphQL API silently fails if you provide it with an invalid time
// string.
// Dates before 1970 (unix epoch) are considered invalid.
startString, endString := "*", "*"
if start.Year() >= 1970 {
startString = start.Format(searchTimeFormat)
}
if end.Year() >= 1970 {
endString = end.Format(searchTimeFormat)
}
return fmt.Sprintf("updated:%s..%s", startString, endString)
}