blob: 77f74864975f8151a9ff952c20fb9e1ce32b220e [file] [log] [blame]
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +00001// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package maintner mirrors, searches, syncs, and serves Git, Github,
6// and Gerrit metadata.
7//
8// Maintner is short for "Maintainer". This package is intended for
9// use by many tools. The name of the daemon that serves the maintner
10// data to other tools is "maintnerd".
11package maintner
12
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000013import (
14 "context"
Brad Fitzpatrick776028b2017-02-15 10:46:56 -080015 "fmt"
16 "io/ioutil"
17 "log"
18 "strings"
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000019 "sync"
20 "time"
21
Brad Fitzpatrick776028b2017-02-15 10:46:56 -080022 "github.com/google/go-github/github"
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000023 "golang.org/x/build/maintner/maintpb"
Brad Fitzpatrick776028b2017-02-15 10:46:56 -080024 "golang.org/x/oauth2"
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000025)
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +000026
27// Corpus holds all of a project's metadata.
28type Corpus struct {
29 // ... TODO
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000030
31 mu sync.RWMutex
Brad Fitzpatrick927294f2017-02-12 21:58:16 +000032 githubIssues map[githubRepo]map[int32]*githubIssue // repo -> num -> issue
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000033 githubUsers map[int64]*githubUser
34}
35
36// githubRepo is a github org & repo, lowercase, joined by a '/',
37// such as "golang/go".
38type githubRepo string
39
40// githubUser represents a github user.
41// It is a subset of https://developer.github.com/v3/users/#get-a-single-user
42type githubUser struct {
Brad Fitzpatrick927294f2017-02-12 21:58:16 +000043 ID int64
44 Login string
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +000045}
46
47// githubIssue represents a github issue.
48// See https://developer.github.com/v3/issues/#get-a-single-issue
49type githubIssue struct {
50 ID int64
51 Number int32
52 Closed bool
53 User *githubUser
54 Created time.Time
55 Updated time.Time
56 Body string
57 // TODO Comments ...
58}
59
60// A MutationSource yields a log of mutations that will catch a corpus
61// back up to the present.
62type MutationSource interface {
63 // GetMutations returns a channel of mutations.
64 // The channel should be closed at the end.
65 // All sends on the returned channel should select
66 // on the provided context.
67 GetMutations(context.Context) <-chan *maintpb.Mutation
68}
69
70func (c *Corpus) processMutations(ctx context.Context, src MutationSource) error {
71 ch := src.GetMutations(ctx)
72 done := ctx.Done()
73
74 c.mu.Lock()
75 defer c.mu.Unlock()
76 for {
77 select {
78 case <-done:
79 return ctx.Err()
80 case m, ok := <-ch:
81 if !ok {
82 return nil
83 }
84 c.processMutationLocked(m)
85 }
86 }
87}
88
89// c.mu must be held.
90func (c *Corpus) processMutationLocked(m *maintpb.Mutation) {
91 if im := m.GithubIssue; im != nil {
92 c.processGithubIssueMutation(im)
93 }
94 // TODO: more...
95}
96
Brad Fitzpatrick927294f2017-02-12 21:58:16 +000097func (c *Corpus) repoKey(owner, repo string) githubRepo {
98 if owner == "" || repo == "" {
99 return ""
100 }
101 // TODO: avoid garbage, use interned strings? profile later
102 // once we have gigabytes of mutation logs to slurp at
103 // start-up. (The same thing mattered for Camlistore start-up
104 // time at least)
105 return githubRepo(owner + "/" + repo)
106}
107
108func (c *Corpus) getGithubUser(pu *maintpb.GithubUser) *githubUser {
109 if pu == nil {
110 return nil
111 }
112 if u := c.githubUsers[pu.Id]; u != nil {
113 if pu.Login != "" && pu.Login != u.Login {
114 u.Login = pu.Login
115 }
116 return u
117 }
118 if c.githubUsers == nil {
119 c.githubUsers = make(map[int64]*githubUser)
120 }
121 u := &githubUser{
122 ID: pu.Id,
123 Login: pu.Login,
124 }
125 c.githubUsers[pu.Id] = u
126 return u
127}
128
Brad Fitzpatrick49a8dd92017-02-12 07:22:59 +0000129func (c *Corpus) processGithubIssueMutation(m *maintpb.GithubIssueMutation) {
Brad Fitzpatrick927294f2017-02-12 21:58:16 +0000130 k := c.repoKey(m.Owner, m.Repo)
131 if k == "" {
132 // TODO: errors? return false? skip for now.
133 return
134 }
135 if m.Number == 0 {
136 return
137 }
138 issueMap, ok := c.githubIssues[k]
139 if !ok {
140 if c.githubIssues == nil {
141 c.githubIssues = make(map[githubRepo]map[int32]*githubIssue)
142 }
143 issueMap = make(map[int32]*githubIssue)
144 c.githubIssues[k] = issueMap
145 }
146 gi, ok := issueMap[m.Number]
147 if !ok {
148 gi = &githubIssue{
149 Number: m.Number,
150 User: c.getGithubUser(m.User),
151 }
152 issueMap[m.Number] = gi
153 }
154 if m.Body != "" {
155 gi.Body = m.Body
156 }
157 // TODO: times, etc.
Brad Fitzpatrickb1ddf2b2017-02-08 06:05:26 +0000158}
159
160// PopulateFromServer populates the corpus from a maintnerd server.
161func (c *Corpus) PopulateFromServer(ctx context.Context, serverURL string) error {
162 panic("TODO")
163}
164
165// PopulateFromDisk populates the corpus from a set of mutation logs
166// in a local directory.
167func (c *Corpus) PopulateFromDisk(ctx context.Context, dir string) error {
168 panic("TODO")
169}
170
171// PopulateFromAPIs populates the corpus using API calls to
172// the upstream Git, Github, and/or Gerrit servers.
173func (c *Corpus) PopulateFromAPIs(ctx context.Context) error {
174 panic("TODO")
175}
Brad Fitzpatrick776028b2017-02-15 10:46:56 -0800176
177func (c *Corpus) PollGithubLoop(owner, repo, tokenFile string) error {
178 slurp, err := ioutil.ReadFile(tokenFile)
179 if err != nil {
180 return err
181 }
182 f := strings.SplitN(strings.TrimSpace(string(slurp)), ":", 2)
183 if len(f) != 2 || f[0] == "" || f[1] == "" {
184 return fmt.Errorf("Expected token file %s to be of form <username>:<token>", tokenFile)
185 }
186 ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: f[1]})
187 tc := oauth2.NewClient(oauth2.NoContext, ts)
188 ghc := github.NewClient(tc)
189 for {
190 err := c.pollGithub(owner, repo, ghc)
191 log.Printf("Polled github for %s/%s; err = %v. Sleeping.", owner, repo, err)
192 time.Sleep(30 * time.Second)
193 }
194}
195
196func (c *Corpus) pollGithub(owner, repo string, ghc *github.Client) error {
197 log.Printf("Polling github for %s/%s ...", owner, repo)
198 page := 1
199 keepGoing := true
200 for keepGoing {
201 // TODO: use https://godoc.org/github.com/google/go-github/github#ActivityService.ListIssueEventsForRepository probably
202 issues, res, err := ghc.Issues.ListByRepo(owner, repo, &github.IssueListByRepoOptions{
203 State: "all",
204 Sort: "updated",
205 Direction: "desc",
206 ListOptions: github.ListOptions{
207 Page: page,
208 PerPage: 100,
209 },
210 })
211 if err != nil {
212 return err
213 }
214 log.Printf("github %s/%s: page %d, num issues %d, res: %#v", owner, repo, page, len(issues), res)
215 keepGoing = false
216 for _, is := range issues {
217 _ = is
218 changes := false
219 if changes {
220 keepGoing = true
221 }
222 }
223 page++
224 }
225 return nil
226}