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  }