Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split Bitbucket Server comments if over max length #372

Merged
merged 1 commit into from
Dec 4, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,17 @@ import (
"regexp"
"strings"

"github.com/runatlantis/atlantis/server/events/vcs/common"

"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
"gopkg.in/go-playground/validator.v9"
)

// maxCommentLength is the maximum number of chars allowed by Bitbucket in a
// single comment.
const maxCommentLength = 32768

type Client struct {
HttpClient *http.Client
Username string
Expand Down Expand Up @@ -117,8 +123,22 @@ func (b *Client) GetProjectKey(repoName string, cloneURL string) (string, error)
return matches[1], nil
}

// CreateComment creates a comment on the merge request.
// CreateComment creates a comment on the merge request. It will write multiple
// comments if a single comment is too long.
func (b *Client) CreateComment(repo models.Repo, pullNum int, comment string) error {
sepEnd := "\n```\n**Warning**: Output length greater than max comment size. Continued in next comment."
sepStart := "Continued from previous comment.\n```diff\n"
comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart)
for _, c := range comments {
if err := b.postComment(repo, pullNum, c); err != nil {
return err
}
}
return nil
}

// postComment actually posts the comment. It's a helper for CreateComment().
func (b *Client) postComment(repo models.Repo, pullNum int, comment string) error {
bodyBytes, err := json.Marshal(map[string]string{"text": comment})
if err != nil {
return errors.Wrap(err, "json encoding")
Expand Down
37 changes: 37 additions & 0 deletions server/events/vcs/common/comment_splitter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package common

import (
"math"
)

// SplitComment splits comment into a slice of comments that are under maxSize.
// It appends sepEnd to all comments that have a following comment.
// It prepends sepStart to all comments that have a preceding comment.
func SplitComment(comment string, maxSize int, sepEnd string, sepStart string) []string {
if len(comment) <= maxSize {
return []string{comment}
}

maxWithSep := maxSize - len(sepEnd) - len(sepStart)
var comments []string
numComments := int(math.Ceil(float64(len(comment)) / float64(maxWithSep)))
for i := 0; i < numComments; i++ {
upTo := min(len(comment), (i+1)*maxWithSep)
portion := comment[i*maxWithSep : upTo]
if i < numComments-1 {
portion += sepEnd
}
if i > 0 {
portion = sepStart + portion
}
comments = append(comments, portion)
}
return comments
}

func min(a, b int) int {
if a < b {
return a
}
return b
}
63 changes: 63 additions & 0 deletions server/events/vcs/common/comment_splitter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 common_test

import (
"strings"
"testing"

"github.com/runatlantis/atlantis/server/events/vcs/common"

. "github.com/runatlantis/atlantis/testing"
)

// If under the maximum number of chars, we shouldn't split the comments.
func TestSplitComment_UnderMax(t *testing.T) {
comment := "comment under max size"
split := common.SplitComment(comment, len(comment)+1, "sepEnd", "sepStart")
Equals(t, []string{comment}, split)
}

// If the comment needs to be split into 2 we should do the split and add the
// separators properly.
func TestSplitComment_TwoComments(t *testing.T) {
comment := strings.Repeat("a", 1000)
sepEnd := "-sepEnd"
sepStart := "-sepStart"
split := common.SplitComment(comment, len(comment)-1, sepEnd, sepStart)

expCommentLen := len(comment) - len(sepEnd) - len(sepStart) - 1
expFirstComment := comment[:expCommentLen]
expSecondComment := comment[expCommentLen:]
Equals(t, 2, len(split))
Equals(t, expFirstComment+sepEnd, split[0])
Equals(t, sepStart+expSecondComment, split[1])
}

// If the comment needs to be split into 4 we should do the split and add the
// separators properly.
func TestSplitComment_FourComments(t *testing.T) {
comment := strings.Repeat("a", 1000)
sepEnd := "-sepEnd"
sepStart := "-sepStart"
max := (len(comment) / 4) + len(sepEnd) + len(sepStart)
split := common.SplitComment(comment, max, sepEnd, sepStart)

expMax := len(comment) / 4
Equals(t, []string{
comment[:expMax] + sepEnd,
sepStart + comment[expMax:expMax*2] + sepEnd,
sepStart + comment[expMax*2:expMax*3] + sepEnd,
sepStart + comment[expMax*3:]}, split)
}
58 changes: 11 additions & 47 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,19 @@ package vcs
import (
"context"
"fmt"
"math"
"net/url"
"strings"

"github.com/runatlantis/atlantis/server/events/vcs/common"

"github.com/google/go-github/github"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events/models"
)

// detailsClose is appended to a comment that is so long we split it into
// multiple comments.
const detailsClose = "\n```\n</details>" +
"\n<br>\n\n**Warning**: Output length greater than max comment size. Continued in next comment."

// detailsOpen is prepended to the following comments when we split.
const detailsOpen = "Continued from previous comment.\n<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)
// maxCommentLength is the maximum number of chars allowed in a single comment
// by GitHub.
const maxCommentLength = 65536

// GithubClient is used to perform GitHub actions.
type GithubClient struct {
Expand Down Expand Up @@ -101,7 +92,12 @@ func (g *GithubClient) GetModifiedFiles(repo models.Repo, pull models.PullReques
// 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)
sepEnd := "\n```\n</details>" +
"\n<br>\n\n**Warning**: Output length greater than max comment size. Continued in next comment."
sepStart := "Continued from previous comment.\n<details><summary>Show Output</summary>\n\n" +
"```diff\n"

comments := common.SplitComment(comment, maxCommentLength, sepEnd, sepStart)
for _, c := range comments {
_, _, err := g.client.Issues.CreateComment(g.ctx, repo.Owner, repo.Name, pullNum, &github.IssueComment{Body: &c})
if err != nil {
Expand Down Expand Up @@ -151,35 +147,3 @@ func (g *GithubClient) UpdateStatus(repo models.Repo, pull models.PullRequest, s
_, _, 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
}
29 changes: 0 additions & 29 deletions server/events/vcs/github_client_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,6 @@ import (
. "github.com/runatlantis/atlantis/testing"
)

// If under the maximum number of chars, we shouldn't split the comments.
func TestSplitAtMaxChars_UnderMax(t *testing.T) {
client := &GithubClient{}
comment := "comment under max size"
split := client.splitAtMaxChars(comment, len(comment)+1)
Equals(t, []string{comment}, split)
}

// If the comment is over the max number of chars, we should split it into
// multiple comments.
func TestSplitAtMaxChars_OverMaxOnce(t *testing.T) {
client := &GithubClient{}
comment := "comment over max size"
split := client.splitAtMaxChars(comment, len(comment)-1)
Equals(t, []string{"comment over max siz" + detailsClose, detailsOpen + "e"}, split)
}

// Test that it works for multiple comments.
func TestSplitAtMaxChars_OverMaxMultiple(t *testing.T) {
client := &GithubClient{}
comment := "comment over max size"
third := len(comment) / 3
split := client.splitAtMaxChars(comment, third)
Equals(t, []string{
comment[:third] + detailsClose,
detailsOpen + comment[third:third*2] + detailsClose,
detailsOpen + comment[third*2:]}, split)
}

// If the hostname is github.com, should use normal BaseURL.
func TestNewGithubClient_GithubCom(t *testing.T) {
client, err := NewGithubClient("github.com", "user", "pass")
Expand Down