From bf19968cec669c6033064bd1307fa47b78bbb9d8 Mon Sep 17 00:00:00 2001
From: Luke Kysow <1034429+lkysow@users.noreply.github.com>
Date: Tue, 4 Dec 2018 09:51:15 -0600
Subject: [PATCH] Split Bitbucket Server comments if over max length
- Refactor the comment split method into a common package
---
server/events/vcs/bitbucketserver/client.go | 22 ++++++-
server/events/vcs/common/comment_splitter.go | 37 +++++++++++
.../vcs/common/comment_splitter_test.go | 63 +++++++++++++++++++
server/events/vcs/github_client.go | 58 ++++-------------
.../events/vcs/github_client_internal_test.go | 29 ---------
5 files changed, 132 insertions(+), 77 deletions(-)
create mode 100644 server/events/vcs/common/comment_splitter.go
create mode 100644 server/events/vcs/common/comment_splitter_test.go
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.\nShow 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.\nShow 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")