Brad Fitzpatrick | b1ddf2b | 2017-02-08 06:05:26 +0000 | [diff] [blame] | 1 | // 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". |
| 11 | package maintner |
| 12 | |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 13 | import ( |
| 14 | "context" |
Brad Fitzpatrick | 776028b | 2017-02-15 10:46:56 -0800 | [diff] [blame^] | 15 | "fmt" |
| 16 | "io/ioutil" |
| 17 | "log" |
| 18 | "strings" |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 19 | "sync" |
| 20 | "time" |
| 21 | |
Brad Fitzpatrick | 776028b | 2017-02-15 10:46:56 -0800 | [diff] [blame^] | 22 | "github.com/google/go-github/github" |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 23 | "golang.org/x/build/maintner/maintpb" |
Brad Fitzpatrick | 776028b | 2017-02-15 10:46:56 -0800 | [diff] [blame^] | 24 | "golang.org/x/oauth2" |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 25 | ) |
Brad Fitzpatrick | b1ddf2b | 2017-02-08 06:05:26 +0000 | [diff] [blame] | 26 | |
| 27 | // Corpus holds all of a project's metadata. |
| 28 | type Corpus struct { |
| 29 | // ... TODO |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 30 | |
| 31 | mu sync.RWMutex |
Brad Fitzpatrick | 927294f | 2017-02-12 21:58:16 +0000 | [diff] [blame] | 32 | githubIssues map[githubRepo]map[int32]*githubIssue // repo -> num -> issue |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 33 | githubUsers map[int64]*githubUser |
| 34 | } |
| 35 | |
| 36 | // githubRepo is a github org & repo, lowercase, joined by a '/', |
| 37 | // such as "golang/go". |
| 38 | type 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 |
| 42 | type githubUser struct { |
Brad Fitzpatrick | 927294f | 2017-02-12 21:58:16 +0000 | [diff] [blame] | 43 | ID int64 |
| 44 | Login string |
Brad Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 45 | } |
| 46 | |
| 47 | // githubIssue represents a github issue. |
| 48 | // See https://developer.github.com/v3/issues/#get-a-single-issue |
| 49 | type 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. |
| 62 | type 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 | |
| 70 | func (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. |
| 90 | func (c *Corpus) processMutationLocked(m *maintpb.Mutation) { |
| 91 | if im := m.GithubIssue; im != nil { |
| 92 | c.processGithubIssueMutation(im) |
| 93 | } |
| 94 | // TODO: more... |
| 95 | } |
| 96 | |
Brad Fitzpatrick | 927294f | 2017-02-12 21:58:16 +0000 | [diff] [blame] | 97 | func (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 | |
| 108 | func (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 Fitzpatrick | 49a8dd9 | 2017-02-12 07:22:59 +0000 | [diff] [blame] | 129 | func (c *Corpus) processGithubIssueMutation(m *maintpb.GithubIssueMutation) { |
Brad Fitzpatrick | 927294f | 2017-02-12 21:58:16 +0000 | [diff] [blame] | 130 | 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 Fitzpatrick | b1ddf2b | 2017-02-08 06:05:26 +0000 | [diff] [blame] | 158 | } |
| 159 | |
| 160 | // PopulateFromServer populates the corpus from a maintnerd server. |
| 161 | func (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. |
| 167 | func (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. |
| 173 | func (c *Corpus) PopulateFromAPIs(ctx context.Context) error { |
| 174 | panic("TODO") |
| 175 | } |
Brad Fitzpatrick | 776028b | 2017-02-15 10:46:56 -0800 | [diff] [blame^] | 176 | |
| 177 | func (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 | |
| 196 | func (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 | } |