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 }