github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/repotracker/github_poller.go (about)

     1  package repotracker
     2  
     3  import (
     4  	"encoding/base64"
     5  	"fmt"
     6  	"net/http"
     7  	"time"
     8  
     9  	"github.com/evergreen-ci/evergreen/model"
    10  	"github.com/evergreen-ci/evergreen/thirdparty"
    11  	"github.com/pkg/errors"
    12  )
    13  
    14  // GithubRepositoryPoller is a struct that implements Github specific behavior
    15  // required of a RepoPoller
    16  type GithubRepositoryPoller struct {
    17  	ProjectRef *model.ProjectRef
    18  	OauthToken string
    19  }
    20  
    21  // NewGithubRepositoryPoller constructs and returns a pointer to a
    22  //GithubRepositoryPoller struct
    23  func NewGithubRepositoryPoller(projectRef *model.ProjectRef,
    24  	oauthToken string) *GithubRepositoryPoller {
    25  	return &GithubRepositoryPoller{
    26  		ProjectRef: projectRef,
    27  		OauthToken: oauthToken,
    28  	}
    29  }
    30  
    31  // GetCommitURL constructs the required URL to query for Github commits
    32  func getCommitURL(projectRef *model.ProjectRef) string {
    33  	return fmt.Sprintf("https://api.github.com/repos/%v/%v/commits?sha=%v",
    34  		projectRef.Owner,
    35  		projectRef.Repo,
    36  		projectRef.Branch,
    37  	)
    38  }
    39  
    40  // isLastRevision compares a Github Commit's sha with a revision and returns
    41  // true if they are the same
    42  func isLastRevision(revision string, githubCommit *thirdparty.GithubCommit) bool {
    43  	return githubCommit.SHA == revision
    44  }
    45  
    46  // githubCommitToRevision converts a GithubCommit struct to a
    47  // model.Revision struct
    48  func githubCommitToRevision(
    49  	githubCommit *thirdparty.GithubCommit) model.Revision {
    50  	return model.Revision{
    51  		Author:          githubCommit.Commit.Author.Name,
    52  		AuthorEmail:     githubCommit.Commit.Author.Email,
    53  		RevisionMessage: githubCommit.Commit.Message,
    54  		Revision:        githubCommit.SHA,
    55  		CreateTime:      time.Now(),
    56  	}
    57  }
    58  
    59  // GetRemoteConfig fetches the contents of a remote github repository's
    60  // configuration data as at a given revision
    61  func (gRepoPoller *GithubRepositoryPoller) GetRemoteConfig(
    62  	projectFileRevision string) (projectConfig *model.Project, err error) {
    63  	// find the project configuration file for the given repository revision
    64  	projectRef := gRepoPoller.ProjectRef
    65  	projectFileURL := thirdparty.GetGithubFileURL(
    66  		projectRef.Owner,
    67  		projectRef.Repo,
    68  		projectRef.RemotePath,
    69  		projectFileRevision,
    70  	)
    71  
    72  	githubFile, err := thirdparty.GetGithubFile(
    73  		gRepoPoller.OauthToken,
    74  		projectFileURL,
    75  	)
    76  	if err != nil {
    77  		return nil, err
    78  	}
    79  
    80  	projectFileBytes, err := base64.StdEncoding.DecodeString(githubFile.Content)
    81  	if err != nil {
    82  		return nil, thirdparty.FileDecodeError{err.Error()}
    83  	}
    84  
    85  	projectConfig = &model.Project{}
    86  	err = model.LoadProjectInto(projectFileBytes, projectRef.Identifier, projectConfig)
    87  	if err != nil {
    88  		return nil, thirdparty.YAMLFormatError{err.Error()}
    89  	}
    90  
    91  	return projectConfig, nil
    92  }
    93  
    94  // GetRemoteConfig fetches the contents of a remote github repository's
    95  // configuration data as at a given revision
    96  func (gRepoPoller *GithubRepositoryPoller) GetChangedFiles(commitRevision string) ([]string, error) {
    97  	// get the entire commit, then pull the files from it
    98  	projectRef := gRepoPoller.ProjectRef
    99  	commit, err := thirdparty.GetCommitEvent(
   100  		gRepoPoller.OauthToken,
   101  		projectRef.Owner,
   102  		projectRef.Repo,
   103  		commitRevision,
   104  	)
   105  	if err != nil {
   106  		return nil, errors.Wrapf(err, "error loading commit '%v'", commitRevision)
   107  	}
   108  	files := []string{}
   109  	for _, f := range commit.Files {
   110  		files = append(files, f.FileName)
   111  	}
   112  	return files, nil
   113  }
   114  
   115  // GetRevisionsSince fetches the all commits from the corresponding Github
   116  // ProjectRef that were made after 'revision'
   117  func (gRepoPoller *GithubRepositoryPoller) GetRevisionsSince(
   118  	revision string, maxRevisionsToSearch int) ([]model.Revision, error) {
   119  
   120  	var foundLatest bool
   121  	var commits []thirdparty.GithubCommit
   122  	var firstCommit *thirdparty.GithubCommit // we track this for later error handling
   123  	var header http.Header
   124  	commitURL := getCommitURL(gRepoPoller.ProjectRef)
   125  	revisions := []model.Revision{}
   126  
   127  	for len(revisions) < maxRevisionsToSearch {
   128  		var err error
   129  		commits, header, err = thirdparty.GetGithubCommits(gRepoPoller.OauthToken, commitURL)
   130  		if err != nil {
   131  			return nil, err
   132  		}
   133  
   134  		for i := range commits {
   135  			commit := &commits[i]
   136  			if firstCommit == nil {
   137  				firstCommit = commit
   138  			}
   139  			if isLastRevision(revision, commit) {
   140  				foundLatest = true
   141  				break
   142  			}
   143  			revisions = append(revisions, githubCommitToRevision(commit))
   144  		}
   145  
   146  		// stop querying for commits if we've found the latest commit or got back no commits
   147  		if foundLatest || len(revisions) == 0 {
   148  			break
   149  		}
   150  
   151  		// stop quering for commits if there's no next page
   152  		if commitURL = thirdparty.NextGithubPageLink(header); commitURL == "" {
   153  			break
   154  		}
   155  	}
   156  
   157  	if !foundLatest {
   158  		var revisionDetails *model.RepositoryErrorDetails
   159  		var revisionError error
   160  		// attempt to get the merge base commit
   161  		baseRevision, err := thirdparty.GetGitHubMergeBaseRevision(
   162  			gRepoPoller.OauthToken,
   163  			gRepoPoller.ProjectRef.Owner,
   164  			gRepoPoller.ProjectRef.Repo,
   165  			revision,
   166  			firstCommit,
   167  		)
   168  		if len(revision) < 10 {
   169  			return nil, errors.Errorf("invalid revision: %v", revision)
   170  		}
   171  		if err != nil {
   172  			// unable to get merge base commit so set projectRef revision details with a blank base revision
   173  			revisionDetails = &model.RepositoryErrorDetails{
   174  				Exists:            true,
   175  				InvalidRevision:   revision[:10],
   176  				MergeBaseRevision: "",
   177  			}
   178  			revisionError = errors.Wrapf(err,
   179  				"unable to find a suggested merge base commit for revision %v, must fix on projects settings page",
   180  				revision)
   181  		} else {
   182  			// update project ref to have an inconsistent status
   183  			revisionDetails = &model.RepositoryErrorDetails{
   184  				Exists:            true,
   185  				InvalidRevision:   revision[:10],
   186  				MergeBaseRevision: baseRevision,
   187  			}
   188  			revisionError = errors.Errorf("base revision, %v not found, suggested base revision, %v found, must confirm on project settings page",
   189  				revision, baseRevision)
   190  		}
   191  
   192  		gRepoPoller.ProjectRef.RepotrackerError = revisionDetails
   193  		if err = gRepoPoller.ProjectRef.Upsert(); err != nil {
   194  			return []model.Revision{}, errors.Wrap(err, "unable to update projectRef revision details")
   195  		}
   196  
   197  		return []model.Revision{}, revisionError
   198  	}
   199  
   200  	return revisions, nil
   201  }
   202  
   203  // GetRecentRevisions fetches the most recent 'numRevisions'
   204  func (gRepoPoller *GithubRepositoryPoller) GetRecentRevisions(maxRevisions int) (
   205  	revisions []model.Revision, err error) {
   206  	commitURL := getCommitURL(gRepoPoller.ProjectRef)
   207  
   208  	for {
   209  		githubCommits, header, err := thirdparty.GetGithubCommits(
   210  			gRepoPoller.OauthToken, commitURL)
   211  		if err != nil {
   212  			return nil, err
   213  		}
   214  
   215  		for _, commit := range githubCommits {
   216  			if len(revisions) == maxRevisions {
   217  				break
   218  			}
   219  			revisions = append(revisions, githubCommitToRevision(
   220  				&commit))
   221  		}
   222  
   223  		// stop querying for commits if we've reached our target or got back no
   224  		// commits
   225  		if len(revisions) == maxRevisions || len(revisions) == 0 {
   226  			break
   227  		}
   228  
   229  		// stop quering for commits if there's no next page
   230  		if commitURL = thirdparty.NextGithubPageLink(header); commitURL == "" {
   231  			break
   232  		}
   233  	}
   234  	return
   235  }