-
Notifications
You must be signed in to change notification settings - Fork 1.1k
/
github_client.go
178 lines (164 loc) · 5.47 KB
/
github_client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
// Copyright 2017 HootSuite Media Inc.
//
// Licensed under the Apache License, Version 2.0 (the License);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an AS IS BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
//
package vcs
import (
"context"
"fmt"
"math"
"net/url"
"strings"
"github.com/google/go-github/github"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
)
// GithubClient is used to perform GitHub actions.
type GithubClient struct {
client *github.Client
ctx context.Context
}
// NewGithubClient returns a valid GitHub client.
func NewGithubClient(hostname string, user string, pass string) (*GithubClient, error) {
tp := github.BasicAuthTransport{
Username: strings.TrimSpace(user),
Password: strings.TrimSpace(pass),
}
client := github.NewClient(tp.Client())
// If we're using github.com then we don't need to do any additional configuration
// for the client. It we're using Github Enterprise, then we need to manually
// set the base url for the API.
if hostname != "github.com" {
baseURL := fmt.Sprintf("https://%s/api/v3/", hostname)
base, err := url.Parse(baseURL)
if err != nil {
return nil, errors.Wrapf(err, "Invalid github hostname trying to parse %s", baseURL)
}
client.BaseURL = base
}
return &GithubClient{
client: client,
ctx: context.Background(),
}, nil
}
// GetModifiedFiles returns the names of files that were modified in the pull request.
// The names include the path to the file from the repo root, ex. parent/child/file.txt.
func (g *GithubClient) GetModifiedFiles(repo models.Repo, pull models.PullRequest) ([]string, error) {
var files []string
nextPage := 0
for {
opts := github.ListOptions{
PerPage: 300,
}
if nextPage != 0 {
opts.Page = nextPage
}
pageFiles, resp, err := g.client.PullRequests.ListFiles(g.ctx, repo.Owner, repo.Name, pull.Num, &opts)
if err != nil {
return files, err
}
for _, f := range pageFiles {
files = append(files, f.GetFilename())
}
if resp.NextPage == 0 {
break
}
nextPage = resp.NextPage
}
return files, nil
}
// CreateComment creates a comment on the pull request.
// If comment length is greater than the max comment length we split into
// multiple comments.
func (g *GithubClient) CreateComment(repo models.Repo, pullNum int, comment string) error {
// maxCommentBodySize is derived from the error message when you go over
// this limit.
const maxCommentBodySize = 65536
comments := g.splitAtMaxChars(comment, maxCommentBodySize, "\ncontinued...\n")
for _, c := range comments {
_, _, err := g.client.Issues.CreateComment(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueComment{Body: &c})
if err != nil {
return err
}
}
return nil
}
// PullIsApproved returns true if the pull request was approved.
func (g *GithubClient) PullIsApproved(repo models.Repo, pull models.PullRequest) (bool, error) {
reviews, _, err := g.client.PullRequests.ListReviews(g.ctx, repo.Owner, repo.Name, pull.Num, nil)
if err != nil {
return false, errors.Wrap(err, "getting reviews")
}
for _, review := range reviews {
if review != nil && review.GetState() == "APPROVED" {
return true, nil
}
}
return false, nil
}
// GetPullRequest returns the pull request.
func (g *GithubClient) GetPullRequest(repo models.Repo, num int) (*github.PullRequest, error) {
pull, _, err := g.client.PullRequests.Get(g.ctx, repo.Owner, repo.Name, num)
return pull, err
}
// UpdateStatus updates the status badge on the pull request.
// See https://github.com/blog/1227-commit-status-api.
func (g *GithubClient) UpdateStatus(repo models.Repo, pull models.PullRequest, state CommitStatus, description string) error {
const statusContext = "Atlantis"
ghState := "error"
switch state {
case Pending:
ghState = "pending"
case Success:
ghState = "success"
case Failed:
ghState = "failure"
}
status := &github.RepoStatus{
State: github.String(ghState),
Description: github.String(description),
Context: github.String(statusContext)}
_, _, err := g.client.Repositories.CreateStatus(g.ctx, repo.Owner, repo.Name, pull.HeadCommit, status)
return err
}
// splitAtMaxChars splits comment into a slice with string up to max
// len separated by join which gets appended to the ends of the middle strings.
// If max <= len(join) we return an empty slice since this is an edge case we
// don't want to handle.
func (g *GithubClient) splitAtMaxChars(comment string, max int, join string) []string {
// If we're under the limit then no need to split.
if len(comment) <= max {
return []string{comment}
}
// If we can't fit the joining string in then this doesn't make sense.
if max <= len(join) {
return nil
}
var comments []string
maxSize := max - len(join)
numComments := int(math.Ceil(float64(len(comment)) / float64(maxSize)))
for i := 0; i < numComments; i++ {
upTo := g.min(len(comment), (i+1)*maxSize)
portion := comment[i*maxSize : upTo]
if i < numComments-1 {
portion += join
}
comments = append(comments, portion)
}
return comments
}
func (g *GithubClient) min(a, b int) int {
if a < b {
return a
}
return b
}