@@ -12,13 +12,15 @@ package maintner
12
12
13
13
import (
14
14
"context"
15
+ "errors"
15
16
"fmt"
16
17
"io/ioutil"
17
18
"log"
18
19
"strings"
19
20
"sync"
20
21
"time"
21
22
23
+ "github.com/golang/protobuf/ptypes"
22
24
"github.com/google/go-github/github"
23
25
"golang.org/x/build/maintner/maintpb"
24
26
"golang.org/x/oauth2"
@@ -75,6 +77,24 @@ func (c *Corpus) AddGithub(owner, repo, tokenFile string) {
75
77
// such as "golang/go".
76
78
type githubRepo string
77
79
80
+ // Org finds "golang" in the githubRepo string "golang/go", or returns an empty
81
+ // string if it is malformed.
82
+ func (gr githubRepo ) Org () string {
83
+ sep := strings .IndexByte (string (gr ), '/' )
84
+ if sep == - 1 {
85
+ return ""
86
+ }
87
+ return string (gr [:sep ])
88
+ }
89
+
90
+ func (gr githubRepo ) Repo () string {
91
+ sep := strings .IndexByte (string (gr ), '/' )
92
+ if sep == - 1 || sep == len (gr )- 1 {
93
+ return ""
94
+ }
95
+ return string (gr [sep + 1 :])
96
+ }
97
+
78
98
// githubUser represents a github user.
79
99
// It is a subset of https://developer.github.com/v3/users/#get-a-single-user
80
100
type githubUser struct {
@@ -169,7 +189,80 @@ func (c *Corpus) getGithubUser(pu *maintpb.GithubUser) *githubUser {
169
189
return u
170
190
}
171
191
172
- func (c * Corpus ) processGithubIssueMutation (m * maintpb.GithubIssueMutation ) {
192
+ var errNoChanges = errors .New ("No changes in this github.Issue" )
193
+
194
+ // newMutationFromIssue generates a GithubIssueMutation using the smallest
195
+ // possible diff between ci (a corpus Issue) and gi (an external github issue).
196
+ //
197
+ // If newMutationFromIssue returns nil, the provided github.Issue is no newer
198
+ // than the data we have in the corpus. ci may be nil.
199
+ func newMutationFromIssue (ci * githubIssue , gi * github.Issue , rp githubRepo ) * maintpb.GithubIssueMutation {
200
+ if gi == nil || gi .Number == nil {
201
+ panic (fmt .Sprintf ("github issue with nil number: %#v" , gi ))
202
+ }
203
+ owner , repo := rp .Org (), rp .Repo ()
204
+ // always need these fields to figure out which key to write to
205
+ m := & maintpb.GithubIssueMutation {
206
+ Owner : owner ,
207
+ Repo : repo ,
208
+ Number : int32 (* gi .Number ),
209
+ }
210
+ if ci == nil {
211
+ // We don't know about this github issue, so populate all fields in one
212
+ // mutation.
213
+ if gi .CreatedAt != nil {
214
+ tproto , err := ptypes .TimestampProto (* gi .CreatedAt )
215
+ if err != nil {
216
+ panic (err )
217
+ }
218
+ m .Created = tproto
219
+ }
220
+ if gi .UpdatedAt != nil {
221
+ tproto , err := ptypes .TimestampProto (* gi .UpdatedAt )
222
+ if err != nil {
223
+ panic (err )
224
+ }
225
+ m .Updated = tproto
226
+ }
227
+ if gi .Body != nil {
228
+ m .Body = * gi .Body
229
+ }
230
+ return m
231
+ }
232
+ if gi .UpdatedAt != nil {
233
+ if gi .UpdatedAt .Before (ci .Updated ) {
234
+ // This data is stale, ignore it.
235
+ return nil
236
+ }
237
+ tproto , err := ptypes .TimestampProto (* gi .UpdatedAt )
238
+ if err != nil {
239
+ panic (err )
240
+ }
241
+ m .Updated = tproto
242
+ }
243
+ if gi .Body != nil && * gi .Body != ci .Body {
244
+ m .Body = * gi .Body
245
+ }
246
+ return m
247
+ }
248
+
249
+ // getIssue finds an issue in the Corpus or returns nil, false if it is not
250
+ // present.
251
+ func (c * Corpus ) getIssue (rp githubRepo , number int32 ) (* githubIssue , bool ) {
252
+ issueMap , ok := c .githubIssues [rp ]
253
+ if ! ok {
254
+ return nil , false
255
+ }
256
+ gi , ok := issueMap [number ]
257
+ return gi , ok
258
+ }
259
+
260
+ // processGithubIssueMutation updates the corpus with the information in m, and
261
+ // returns true if the Corpus was modified.
262
+ func (c * Corpus ) processGithubIssueMutation (m * maintpb.GithubIssueMutation ) (changed bool ) {
263
+ if c == nil {
264
+ panic ("nil corpus" )
265
+ }
173
266
k := c .repoKey (m .Owner , m .Repo )
174
267
if k == "" {
175
268
// TODO: errors? return false? skip for now.
@@ -188,16 +281,39 @@ func (c *Corpus) processGithubIssueMutation(m *maintpb.GithubIssueMutation) {
188
281
}
189
282
gi , ok := issueMap [m .Number ]
190
283
if ! ok {
284
+ created , err := ptypes .Timestamp (m .Created )
285
+ if err != nil {
286
+ panic (err )
287
+ }
191
288
gi = & githubIssue {
192
- Number : m .Number ,
193
- User : c .getGithubUser (m .User ),
289
+ Number : m .Number ,
290
+ User : c .getGithubUser (m .User ),
291
+ Created : created ,
194
292
}
195
293
issueMap [m .Number ] = gi
294
+ changed = true
295
+ }
296
+ // Check Updated before all other fields so they don't update if this
297
+ // Mutation is stale
298
+ if m .Updated != nil {
299
+ updated , err := ptypes .Timestamp (m .Updated )
300
+ if err != nil {
301
+ panic (err )
302
+ }
303
+ if ! updated .IsZero () && updated .Before (gi .Updated ) {
304
+ // this mutation represents data older than the data we have in
305
+ // the corpus; ignore it.
306
+ return false
307
+ }
308
+ gi .Updated = updated
309
+ changed = changed || updated .After (gi .Updated )
196
310
}
197
311
if m .Body != "" {
198
312
gi .Body = m .Body
313
+ changed = changed || m .Body != gi .Body
199
314
}
200
- // TODO: times, etc.
315
+ // ignoring Created since it *should* never update
316
+ return changed
201
317
}
202
318
203
319
// PopulateFromServer populates the corpus from a maintnerd server.
@@ -257,14 +373,16 @@ func (c *Corpus) pollGithub(ctx context.Context, rp githubRepo, ghc *github.Clie
257
373
log .Printf ("Polling github for %s ..." , rp )
258
374
page := 1
259
375
keepGoing := true
260
- splits := strings .SplitN (string (rp ), "/" , 2 )
261
- owner , repo := splits [0 ], splits [1 ]
376
+ owner , repo := rp .Org (), rp .Repo ()
262
377
for keepGoing {
263
378
// TODO: use https://godoc.org/github.com/google/go-github/github#ActivityService.ListIssueEventsForRepository probably
264
- issues , res , err := ghc .Issues .ListByRepo (context . TODO () , owner , repo , & github.IssueListByRepoOptions {
379
+ issues , _ , err := ghc .Issues .ListByRepo (ctx , owner , repo , & github.IssueListByRepoOptions {
265
380
State : "all" ,
266
381
Sort : "updated" ,
267
382
Direction : "desc" ,
383
+ // TODO: if an issue gets updated while we are paging, we might
384
+ // process the same issue twice - as item 100 on page 1 and then
385
+ // again as item 1 on page 2.
268
386
ListOptions : github.ListOptions {
269
387
Page : page ,
270
388
PerPage : 100 ,
@@ -273,15 +391,22 @@ func (c *Corpus) pollGithub(ctx context.Context, rp githubRepo, ghc *github.Clie
273
391
if err != nil {
274
392
return err
275
393
}
276
- log .Printf ("github %s/%s: page %d, num issues %d, res: %#v" , owner , repo , page , len (issues ), res )
277
- keepGoing = false
394
+ log .Printf ("github %s/%s: page %d, num issues %d" , owner , repo , page , len (issues ))
395
+ if len (issues ) == 0 {
396
+ break
397
+ }
398
+ c .mu .Lock ()
278
399
for _ , is := range issues {
279
400
fmt .Printf ("issue %d: %s\n " , is .ID , * is .Title )
280
- changes := false
281
- if changes {
282
- keepGoing = true
401
+ gi , _ := c .getIssue (rp , int32 (* is .Number ))
402
+ mp := newMutationFromIssue (gi , is , rp )
403
+ if mp == nil {
404
+ keepGoing = false
405
+ break
283
406
}
407
+ c .processGithubIssueMutation (mp )
284
408
}
409
+ c .mu .Unlock ()
285
410
page ++
286
411
}
287
412
return nil
0 commit comments