diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index e7def0edeb..15a7a45dc5 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -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 @@ -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") diff --git a/server/events/vcs/common/comment_splitter.go b/server/events/vcs/common/comment_splitter.go new file mode 100644 index 0000000000..170f3e8049 --- /dev/null +++ b/server/events/vcs/common/comment_splitter.go @@ -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 +} diff --git a/server/events/vcs/common/comment_splitter_test.go b/server/events/vcs/common/comment_splitter_test.go new file mode 100644 index 0000000000..79dea1cb73 --- /dev/null +++ b/server/events/vcs/common/comment_splitter_test.go @@ -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) +} diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index e784fdaa02..1e80b5d668 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -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" + - "\n
\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
Show Output\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 { @@ -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
" + + "\n
\n\n**Warning**: Output length greater than max comment size. Continued in next comment." + sepStart := "Continued from previous comment.\n
Show Output\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 { @@ -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 -} diff --git a/server/events/vcs/github_client_internal_test.go b/server/events/vcs/github_client_internal_test.go index e72d2af51f..7ade1f7ac2 100644 --- a/server/events/vcs/github_client_internal_test.go +++ b/server/events/vcs/github_client_internal_test.go @@ -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")