Files
atlantis/server/events/vcs/github_client.go
Tobias Bielohlawek e32a57b6f3 Fix broken format for continued comments on GitHub
Adds close and (re)open <details> tag in between the split portions to 
ensure the markup is correct across multiple comments. This isn't the prettiest, but a close result, under the assumption continued-comments are in 99% within a terraform output anyway...
2018-11-22 15:07:56 -06:00

182 lines
5.6 KiB
Go

// 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"
)
const detailsClose = "```\n</details>" +
"continued...\n"
const detailsOpen = "<details><summary>Show Output</summary>\n\n" +
"```diff\n"
// maxCommentBodySize is derived from the error message when you go over
// this limit.
// We deduct some characters for appending details close/open tag
const maxCommentBodySize = 65536 - len(detailsClose) - len(detailsOpen)
// 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 {
comments := g.splitAtMaxChars(comment, maxCommentBodySize)
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 models.CommitStatus, description string) error {
const statusContext = "Atlantis"
ghState := "error"
switch state {
case models.PendingCommitStatus:
ghState = "pending"
case models.SuccessCommitStatus:
ghState = "success"
case models.FailedCommitStatus:
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.
// nolint: unparam
func (g *GithubClient) splitAtMaxChars(comment string, maxSize int) []string {
// If we're under the limit then no need to split.
if len(comment) <= maxSize {
return []string{comment}
}
var comments []string
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 += detailsClose
}
if i > 0 {
portion = detailsOpen + portion
}
comments = append(comments, portion)
}
return comments
}
func (g *GithubClient) min(a, b int) int {
if a < b {
return a
}
return b
}