github.com/decred/politeia@v1.4.0/politeiawww/legacy/codetracker/github/update.go (about) 1 // Copyright (c) 2020 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package github 6 7 import ( 8 "strings" 9 "time" 10 11 "github.com/decred/politeia/politeiawww/legacy/codetracker" 12 "github.com/decred/politeia/politeiawww/legacy/codetracker/github/api" 13 "github.com/decred/politeia/politeiawww/legacy/codetracker/github/database" 14 "github.com/decred/politeia/politeiawww/legacy/codetracker/github/database/cockroachdb" 15 ) 16 17 // github contains the client that communicates with the github api and an 18 // instance of the codedb that contains all of the pull request/review 19 // information that is fetched. 20 type github struct { 21 tc *api.Client 22 codedb database.Database 23 } 24 25 // New creates a new github tracker that saves is able to communicate with 26 // the Github user/PR/issue API. 27 func New(apiToken, host, rootCert, cert, key string) (*github, error) { 28 var err error 29 g := &github{} 30 g.tc = api.NewClient(apiToken) 31 g.codedb, err = cockroachdb.New(host, rootCert, cert, key) 32 if err == database.ErrNoVersionRecord || err == database.ErrWrongVersion { 33 log.Errorf("New DB failed no version, wrong version: %v", err) 34 return nil, err 35 } else if err != nil { 36 log.Errorf("New DB failed: %v", err) 37 return nil, err 38 } 39 err = g.codedb.Setup() 40 if err != nil { 41 log.Errorf("codeDB setup failed: %v", err) 42 return nil, err 43 } 44 return g, nil 45 } 46 47 // Update fetches all repos from the given organization and updates all 48 // users' information once the info is fully received. If repoRequest is 49 // included then only that repo will be fetched and updated, typically 50 // used for speeding up testing. 51 func (g *github) Update(repos []string, start, end int64) { 52 for _, repo := range repos { 53 log.Infof("%s", repo) 54 orgRepo := strings.Split(repo, "-") 55 org := orgRepo[0] 56 repo = orgRepo[1] 57 log.Infof("Syncing %s/%s", org, repo) 58 59 // Grab latest sync time 60 prs, err := g.tc.FetchPullsRequest(org, repo) 61 if err != nil { 62 log.Errorf("error fetching repo pullrequest %s/%s %v", org, repo, 63 err) 64 continue 65 } 66 67 for _, pr := range prs { 68 // check to see if last updated time was before the given start date 69 if parseTime(pr.UpdatedAt).Before(time.Unix(start, 0)) { 70 continue 71 } 72 if parseTime(pr.UpdatedAt).After(time.Unix(end, 0)) { 73 continue 74 } 75 err := g.updatePullRequest(org, repo, pr, start) 76 if err != nil { 77 log.Errorf("updatePullRequest for %s/%s %v %v", org, repo, 78 pr.Number, err) 79 } 80 } 81 } 82 } 83 84 func (g *github) updatePullRequest(org, repoName string, pr api.PullsRequest, start int64) error { 85 log.Infof("Updating %v/%v/%v ", org, repoName, pr.Number) 86 apiPullRequest, err := g.fetchPullRequest(org, repoName, pr.Number) 87 if err != nil { 88 return err 89 } 90 _, err = g.codedb.PullRequestByID(apiPullRequest.ID) 91 if err == database.ErrNoPullRequestFound { 92 // Add a new entry since there is nothing there now. 93 err = g.codedb.NewPullRequest(apiPullRequest) 94 if err != nil { 95 log.Errorf("error adding new pull request: %v", err) 96 return err 97 } 98 } else if err != nil { 99 log.Errorf("error finding PR in db", err) 100 return err 101 } 102 103 reviews, err := g.fetchPullRequestReviews(org, repoName, pr.Number, 104 apiPullRequest.URL) 105 if err != nil { 106 return err 107 } 108 for _, review := range reviews { 109 _, err := g.codedb.ReviewByID(review.ID) 110 if err == database.ErrNoPullRequestReviewFound { 111 // Add a new entry since there is nothing there now. 112 err = g.codedb.NewPullRequestReview(&review) 113 if err != nil { 114 log.Errorf("error adding new pull request review: %v", err) 115 continue 116 } 117 } else if err != nil { 118 log.Errorf("error finding Pull Request Review in db", err) 119 return err 120 } 121 } 122 123 // This will fetch all commits not already in the DB. 124 commits, err := g.fetchPullRequestCommits(org, repoName, pr.Number) 125 if err != nil { 126 return err 127 } 128 for _, commit := range commits { 129 commit.Repo = repoName 130 commit.Organization = org 131 // Add a new entry since there is nothing there now. 132 err = g.codedb.NewCommit(commit) 133 if err != nil { 134 log.Errorf("error adding new commit: %v %v", commit.SHA, err) 135 } 136 } 137 return nil 138 } 139 140 func (g *github) fetchPullRequest(org, repoName string, prNum int) (*database.PullRequest, error) { 141 apiPR, err := g.tc.FetchPullRequest(org, repoName, prNum) 142 if err != nil { 143 return nil, err 144 } 145 dbPullRequest, err := convertAPIPullRequestToDbPullRequest(apiPR, repoName, 146 org) 147 if err != nil { 148 log.Errorf("error converting api PR to database: %v", err) 149 return nil, err 150 } 151 return dbPullRequest, nil 152 } 153 154 func (g *github) fetchPullRequestReviews(org, repoName string, prNum int, url string) ([]database.PullRequestReview, error) { 155 prReviews, err := g.tc.FetchPullRequestReviews(org, repoName, prNum) 156 if err != nil { 157 return nil, err 158 } 159 160 reviews := convertAPIReviewsToDbReviews(prReviews, repoName, prNum, url) 161 return reviews, nil 162 } 163 164 func (g *github) fetchPullRequestCommits(org, repoName string, prNum int) ([]*database.Commit, error) { 165 hashes, err := g.tc.FetchPullRequestCommitSHAs(org, repoName, prNum) 166 if err != nil { 167 return nil, err 168 } 169 170 neededHashes := make([]string, 0, 1048) // PNOOMA 171 for _, sha := range hashes { 172 _, err := g.codedb.CommitBySHA(sha) 173 if err == database.ErrNoCommitFound { 174 neededHashes = append(neededHashes, sha) 175 } else if err != nil { 176 log.Errorf("error finding commit in db %v %v", sha, err) 177 continue 178 } 179 } 180 181 prCommits, err := g.tc.FetchPullRequestCommits(org, repoName, neededHashes) 182 if err != nil { 183 return nil, err 184 } 185 commits := convertAPICommitsToDbComits(prCommits, org, repoName) 186 return commits, nil 187 } 188 189 // UserInfo provides the converted information from pull requests and 190 // reviews for a given user of a given period of time. 191 func (g *github) UserInfo(user string, year, month int) (*codetracker.UserInformationResult, error) { 192 startDate := time.Date(year, time.Month(month), 0, 0, 0, 0, 0, 193 time.UTC).Unix() 194 endDate := time.Date(year, time.Month(month+1), 0, 0, 0, 0, 0, 195 time.UTC).Unix() 196 dbMergedPRs, err := g.codedb.MergedPullRequestsByUserDates(user, startDate, 197 endDate) 198 if err != nil { 199 return nil, err 200 } 201 dbUpdatedPRs, err := g.codedb.UpdatedPullRequestsByUserDates(user, 202 startDate, endDate) 203 if err != nil { 204 return nil, err 205 } 206 dbCommits, err := g.codedb.CommitsByUserDates(user, startDate, endDate) 207 if err != nil { 208 return nil, err 209 } 210 211 // Now we need to see if there are any other hits in the DB so we can 212 // see if it was an update to an existing PR or if it is new. If it is 213 // new then we just keep the current Additions/Deletions, If it is existing 214 // and no other updates from before start date then we just keep the 215 // Additions/Deletions. If if is existing and the it was before start, then 216 // take the difference between that last update before start and this most 217 // recent update in the current month. The idea here is we want to capture 218 // the work completed in a given month. 219 220 for i, updatedPR := range dbUpdatedPRs { 221 urlPRs, err := g.codedb.PullRequestsByURL(updatedPR.URL) 222 if err != nil { 223 return nil, err 224 } 225 // There are existing PRs 226 if len(urlPRs) > 1 { 227 var lastUpdated *database.PullRequest 228 for _, urlPR := range urlPRs { 229 // Find the most recent PR returned that is before start 230 if urlPR.UpdatedAt < startDate && 231 (lastUpdated == nil || 232 urlPR.UpdatedAt > lastUpdated.UpdatedAt) { 233 lastUpdated = urlPR 234 } 235 } 236 // lastUpdated was found to be before start and was the last updated 237 // so change the pr additions/deletions to the diff so they 238 // can be tabulated accurately. 239 if lastUpdated != nil { 240 updatedPR.Additions = updatedPR.Additions - 241 lastUpdated.Additions 242 updatedPR.Deletions = updatedPR.Deletions - 243 lastUpdated.Deletions 244 dbUpdatedPRs[i] = updatedPR 245 } 246 } 247 } 248 249 dbReviews, err := g.codedb.ReviewsByUserDates(user, startDate, endDate) 250 if err != nil { 251 return nil, err 252 } 253 userInfo := &codetracker.UserInformationResult{} 254 userInfo.MergedPRs = convertDBPullRequestsToPullRequests(dbMergedPRs) 255 userInfo.UpdatedPRs = convertDBPullRequestsToPullRequests(dbUpdatedPRs) 256 userInfo.Reviews = convertDBPullRequestReviewsToReviews(dbReviews) 257 userInfo.Commits = convertDBCommitsToCommits(dbCommits) 258 userInfo.User = user 259 return userInfo, nil 260 } 261 262 func parseTime(tstamp string) time.Time { 263 t, err := time.Parse(time.RFC3339, tstamp) 264 if err != nil { 265 return time.Time{} 266 } 267 return t 268 }