Skip to content

Commit 8c0c9b3

Browse files
committed
fix: add helper example of using the graphql query API
1 parent ccf6b89 commit 8c0c9b3

File tree

5 files changed

+218
-2
lines changed

5 files changed

+218
-2
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/pkg/errors v0.8.1
88
github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260
99
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f // indirect
10+
github.com/sirupsen/logrus v1.4.2 // indirect
1011
github.com/stretchr/testify v1.3.0
1112
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
1213
k8s.io/apimachinery v0.0.0-20190703205208-4cfb76a8bf76

go.sum

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ github.com/h2non/gock v1.0.9/go.mod h1:CZMcB0Lg5IWnr9bF79pPMg9WeV6WumxQiUJ1UvdO1
2323
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
2424
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
2525
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
26+
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
2627
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
2728
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
2829
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -40,8 +41,12 @@ github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260 h1:xKXiRdBUtMVp6
4041
github.com/shurcooL/githubv4 v0.0.0-20190718010115-4ba037080260/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo=
4142
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f h1:tygelZueB1EtXkPI6mQ4o9DQ0+FKW41hTbunoXZCTqk=
4243
github.com/shurcooL/graphql v0.0.0-20181231061246-d48a9a75455f/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg=
44+
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
45+
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
4346
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
4447
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
48+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
49+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
4550
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
4651
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
4752
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -56,6 +61,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
5661
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
5762
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
5863
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
64+
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
65+
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f h1:25KHgbfyiSm6vwQLbM3zZIe1v9p/3ea4Rz+nnM5K/i4=
5966
golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
6067
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
6168
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

scm/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ type (
102102
Reviews ReviewService
103103
Users UserService
104104
Webhooks WebhookService
105-
Query GraphQLService
105+
GraphQL GraphQLService
106106

107107
// DumpResponse optionally specifies a function to
108108
// dump the the response body for debugging purposes.

scm/driver/github/github.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func New(uri string) (*scm.Client, error) {
4949
client.Webhooks = &webhookService{client}
5050

5151
graphqlEndpoint := scm.UrlJoin(uri, "/graphql")
52-
client.Query = &dynamicGraphQLClient{client, graphqlEndpoint}
52+
client.GraphQL = &dynamicGraphQLClient{client, graphqlEndpoint}
5353

5454
return client.Client, nil
5555
}

scm/factory/examples/graphql/main.go

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

0 commit comments

Comments
 (0)