Skip to content

Commit

Permalink
Gitlab Hide Previous Comments (runatlantis#3476)
Browse files Browse the repository at this point in the history
  • Loading branch information
X-Guardian authored Jun 14, 2023
1 parent 6601b0a commit 556de93
Show file tree
Hide file tree
Showing 3 changed files with 182 additions and 3 deletions.
2 changes: 1 addition & 1 deletion runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ This is useful when you have many projects and want to keep the pull request cle
ATLANTIS_HIDE_PREV_PLAN_COMMENTS=true
```
Hide previous plan comments to declutter PRs. This is only supported in
GitHub currently. This is not enabled by default.
GitHub and GitLab currently. This is not enabled by default.

### `--locking-db-type`
```bash
Expand Down
63 changes: 61 additions & 2 deletions server/events/vcs/gitlab_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ import (
)

// gitlabMaxCommentLength is the maximum number of chars allowed by Gitlab in a
// single comment.
const gitlabMaxCommentLength = 1000000
// single comment, reduced by 100 to allow comments to be hidden with a summary header
// and footer.
const gitlabMaxCommentLength = 1000000 - 100

type GitlabClient struct {
Client *gitlab.Client
Expand All @@ -45,6 +46,8 @@ type GitlabClient struct {
PollingInterval time.Duration
// PollingInterval is the total duration for which to poll, where applicable.
PollingTimeout time.Duration
// logger
logger logging.SimpleLogging
}

// commonMarkSupported is a version constraint that is true when this version of
Expand All @@ -60,6 +63,7 @@ func NewGitlabClient(hostname string, token string, logger logging.SimpleLogging
client := &GitlabClient{
PollingInterval: time.Second,
PollingTimeout: time.Second * 30,
logger: logger,
}

// Create the client differently depending on the base URL.
Expand Down Expand Up @@ -187,6 +191,61 @@ func (g *GitlabClient) ReactToComment(repo models.Repo, pullNum int, commentID i
}

func (g *GitlabClient) HidePrevCommandComments(repo models.Repo, pullNum int, command string) error {
var allComments []*gitlab.Note

nextPage := 0
for {
g.logger.Debug("/projects/%v/merge_requests/%d/notes", repo.FullName, pullNum)
comments, resp, err := g.Client.Notes.ListMergeRequestNotes(repo.FullName, pullNum,
&gitlab.ListMergeRequestNotesOptions{
Sort: gitlab.String("asc"),
OrderBy: gitlab.String("created_at"),
ListOptions: gitlab.ListOptions{Page: nextPage},
})
if err != nil {
return errors.Wrap(err, "listing comments")
}
allComments = append(allComments, comments...)
if resp.NextPage == 0 {
break
}
nextPage = resp.NextPage
}

currentUser, _, err := g.Client.Users.CurrentUser()
if err != nil {
return errors.Wrap(err, "error getting currentuser")
}

summaryHeader := fmt.Sprintf("<!--- +-Superseded Command-+ ---><details><summary>Superseded Atlantis %s</summary>", command)
summaryFooter := "</details>"
lineFeed := "\n"

for _, comment := range allComments {
// Only process non-system comments authored by the Atlantis user
if comment.System || (comment.Author.Username != "" && !strings.EqualFold(comment.Author.Username, currentUser.Username)) {
continue
}

body := strings.Split(comment.Body, "\n")
if len(body) == 0 {
continue
}
firstLine := strings.ToLower(body[0])
// Skip processing comments that don't contain the command or contain the summary header in the first line
if !strings.Contains(firstLine, strings.ToLower(command)) || firstLine == strings.ToLower(summaryHeader) {
continue
}

g.logger.Debug("Updating merge request note: Repo: '%s', MR: '%d', comment ID: '%d'", repo.FullName, pullNum, comment.ID)
supersededComment := summaryHeader + lineFeed + comment.Body + lineFeed + summaryFooter + lineFeed

if _, _, err := g.Client.Notes.UpdateMergeRequestNote(repo.FullName, pullNum, comment.ID,
&gitlab.UpdateMergeRequestNoteOptions{Body: &supersededComment}); err != nil {
return errors.Wrapf(err, "updating comment %d", comment.ID)
}
}

return nil
}

Expand Down
120 changes: 120 additions & 0 deletions server/events/vcs/gitlab_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import (
"io"
"net/http"
"net/http/httptest"
"path"
"strings"
"testing"
"time"

version "github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
gitlab "github.com/xanzy/go-gitlab"
Expand Down Expand Up @@ -429,6 +432,123 @@ func TestGitlabClient_MarkdownPullLink(t *testing.T) {
Equals(t, exp, s)
}

func TestGitlabClient_HideOldComments(t *testing.T) {
type notePutCallDetails struct {
noteID string
comment []string
}
type jsonBody struct {
Body string
}

authorID := 1
authorUserName := "pipin"
authorEmail := "[email protected]"
pullNum := 123

userCommentIDs := [1]string{"1"}
planCommentIDs := [2]string{"3", "5"}
systemCommentIDs := [1]string{"4"}
summaryCommentIDs := [1]string{"2"}
planComments := [3]string{"plan comment 1", "plan comment 2", "plan comment 3"}
summaryHeader := fmt.Sprintf("<!--- +-Superseded Command-+ ---><details><summary>Superseded Atlantis %s</summary>",
command.Plan.TitleString())
summaryFooter := "</details>"
lineFeed := "\\n"

issueResp := "[" +
fmt.Sprintf(`{"id":%s,"body":"User comment","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
userCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + "," +
fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
summaryCommentIDs[0], summaryHeader+lineFeed+planComments[2]+lineFeed+summaryFooter, authorID, authorUserName, authorEmail, pullNum) + "," +
fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
planCommentIDs[0], planComments[0], authorID, authorUserName, authorEmail, pullNum) + "," +
fmt.Sprintf(`{"id":%s,"body":"System comment","author":{"id": %d, "username":"%s", "email":"%s"},"system": true,"project_id": %d}`,
systemCommentIDs[0], authorID, authorUserName, authorEmail, pullNum) + "," +
fmt.Sprintf(`{"id":%s,"body":"%s","author":{"id": %d, "username":"%s", "email":"%s"},"system": false,"project_id": %d}`,
planCommentIDs[1], planComments[1], authorID, authorUserName, authorEmail, pullNum) +
"]"

gitlabClientUnderTest = true
defer func() { gitlabClientUnderTest = false }()
gotNotePutCalls := make([]notePutCallDetails, 0, 1)
testServer := httptest.NewServer(
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
switch r.RequestURI {
case "/api/v4/user":
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "application/json")
response := fmt.Sprintf(`{"id": %d,"username": "%s", "email": "%s"}`, authorID, authorUserName, authorEmail)
w.Write([]byte(response)) // nolint: errcheck
case fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes?order_by=created_at&sort=asc", pullNum):
w.WriteHeader(http.StatusOK)
response := issueResp
w.Write([]byte(response)) // nolint: errcheck
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
}
case "PUT":
switch {
case strings.HasPrefix(r.RequestURI, fmt.Sprintf("/api/v4/projects/runatlantis%%2Fatlantis/merge_requests/%d/notes/", pullNum)):
w.WriteHeader(http.StatusOK)
var body jsonBody
json.NewDecoder(r.Body).Decode(&body)
notePutCallDetail := notePutCallDetails{
noteID: path.Base(r.RequestURI),
comment: strings.Split(body.Body, "\n"),
}
gotNotePutCalls = append(gotNotePutCalls, notePutCallDetail)
response := "{}"
w.Write([]byte(response)) // nolint: errcheck
default:
t.Errorf("got unexpected request at %q", r.RequestURI)
http.Error(w, "not found", http.StatusNotFound)
}
default:
t.Errorf("got unexpected method at %q", r.Method)
http.Error(w, "not found", http.StatusNotFound)
}
}),
)

internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL))
Ok(t, err)
client := &GitlabClient{
Client: internalClient,
Version: nil,
logger: logging.NewNoopLogger(t),
}

repo := models.Repo{
FullName: "runatlantis/atlantis",
Owner: "runatlantis",
Name: "atlantis",
VCSHost: models.VCSHost{
Type: models.Gitlab,
Hostname: "gitlab.com",
},
}

err = client.HidePrevCommandComments(repo, pullNum, command.Plan.TitleString())
Ok(t, err)

// Check the correct number of plan comments have been processed
Equals(t, len(planCommentIDs), len(gotNotePutCalls))
// Check the first plan comment has been currectly summarised
Equals(t, planCommentIDs[0], gotNotePutCalls[0].noteID)
Equals(t, summaryHeader, gotNotePutCalls[0].comment[0])
Equals(t, planComments[0], gotNotePutCalls[0].comment[1])
Equals(t, summaryFooter, gotNotePutCalls[0].comment[2])
// Check the second plan comment has been currectly summarised
Equals(t, planCommentIDs[1], gotNotePutCalls[1].noteID)
Equals(t, summaryHeader, gotNotePutCalls[1].comment[0])
Equals(t, planComments[1], gotNotePutCalls[1].comment[1])
Equals(t, summaryFooter, gotNotePutCalls[1].comment[2])
}

var mergeSuccess = `{"id":22461274,"iid":13,"project_id":4580910,"title":"Update main.tf","description":"","state":"merged","created_at":"2019-01-15T18:27:29.375Z","updated_at":"2019-01-25T17:28:01.437Z","merged_by":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"merged_at":"2019-01-25T17:28:01.459Z","closed_by":null,"closed_at":null,"target_branch":"patch-1","source_branch":"patch-1-merger","upvotes":0,"downvotes":0,"author":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"assignee":null,"source_project_id":4580910,"target_project_id":4580910,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","detailed_merge_status":"mergeable","sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha":"c9b336f1c71d3e64810b8cfa2abcfab232d6bff6","user_notes_count":0,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":false,"web_url":"https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"subscribed":true,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"diff_refs":{"base_sha":"67cb91d3f6198189f433c045154a885784ba6977","head_sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha":"67cb91d3f6198189f433c045154a885784ba6977"},"merge_error":null,"approvals_before_merge":null}`
var pipelineSuccess = `{"id": 22461274,"iid": 13,"project_id": 4580910,"title": "Update main.tf","description": "","state": "opened","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","merged_by": null,"merged_at": null,"closed_by": null,"closed_at": null,"target_branch": "patch-1","source_branch": "patch-1-merger","user_notes_count": 0,"upvotes": 0,"downvotes": 0,"author": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"assignee": null,"reviewers": [],"source_project_id": 4580910,"target_project_id": 4580910,"labels": [],"work_in_progress": false,"milestone": null,"merge_when_pipeline_succeeds": false,"merge_status": "can_be_merged","detailed_merge_status": "mergeable","sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha": null,"squash_commit_sha": null,"discussion_locked": null,"should_remove_source_branch": null,"force_remove_source_branch": true,"reference": "!13","references": {"short": "!13","relative": "!13","full": "lkysow/atlantis-example!13"},"web_url": "https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats": {"time_estimate": 0,"total_time_spent": 0,"human_time_estimate": null,"human_total_time_spent": null},"squash": true,"task_completion_status": {"count": 0,"completed_count": 0},"has_conflicts": false,"blocking_discussions_resolved": true,"approvals_before_merge": null,"subscribed": false,"changes_count": "1","latest_build_started_at": "2019-01-15T18:27:29.375Z","latest_build_finished_at": "2019-01-25T17:28:01.437Z","first_deployed_to_production_at": null,"pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598"},"head_pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598","before_sha": "0000000000000000000000000000000000000000","tag": false,"yaml_errors": null,"user": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"started_at": "2019-01-15T18:27:29.375Z","finished_at": "2019-01-25T17:28:01.437Z","committed_at": null,"duration": 31,"coverage": null,"detailed_status": {"icon": "status_success","text": "passed","label": "passed","group": "success","tooltip": "passed","has_details": true,"details_path": "/lkysow/atlantis-example/-/pipelines/488598","illustration": null,"favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"}},"diff_refs": {"base_sha": "67cb91d3f6198189f433c045154a885784ba6977","head_sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha": "67cb91d3f6198189f433c045154a885784ba6977"},"merge_error": null,"first_contribution": false,"user": {"can_merge": true}}`
var projectSuccess = `{"id": 4580910,"description": "","name": "atlantis-example","name_with_namespace": "lkysow / atlantis-example","path": "atlantis-example","path_with_namespace": "lkysow/atlantis-example","created_at": "2018-04-30T13:44:28.367Z","default_branch": "patch-1","tag_list": [],"ssh_url_to_repo": "[email protected]:lkysow/atlantis-example.git","http_url_to_repo": "https://gitlab.com/lkysow/atlantis-example.git","web_url": "https://gitlab.com/lkysow/atlantis-example","readme_url": "https://gitlab.com/lkysow/atlantis-example/-/blob/main/README.md","avatar_url": "https://gitlab.com/uploads/-/system/project/avatar/4580910/avatar.png","forks_count": 0,"star_count": 7,"last_activity_at": "2021-06-29T21:10:43.968Z","namespace": {"id": 1,"name": "lkysow","path": "lkysow","kind": "group","full_path": "lkysow","parent_id": 1,"avatar_url": "/uploads/-/system/group/avatar/1651/platform.png","web_url": "https://gitlab.com/groups/lkysow"},"_links": {"self": "https://gitlab.com/api/v4/projects/4580910","issues": "https://gitlab.com/api/v4/projects/4580910/issues","merge_requests": "https://gitlab.com/api/v4/projects/4580910/merge_requests","repo_branches": "https://gitlab.com/api/v4/projects/4580910/repository/branches","labels": "https://gitlab.com/api/v4/projects/4580910/labels","events": "https://gitlab.com/api/v4/projects/4580910/events","members": "https://gitlab.com/api/v4/projects/4580910/members"},"packages_enabled": false,"empty_repo": false,"archived": false,"visibility": "private","resolve_outdated_diff_discussions": false,"container_registry_enabled": false,"container_expiration_policy": {"cadence": "1d","enabled": false,"keep_n": 10,"older_than": "90d","name_regex": ".*","name_regex_keep": null,"next_run_at": "2021-05-01T13:44:28.397Z"},"issues_enabled": true,"merge_requests_enabled": true,"wiki_enabled": false,"jobs_enabled": true,"snippets_enabled": true,"service_desk_enabled": false,"service_desk_address": null,"can_create_merge_request_in": true,"issues_access_level": "private","repository_access_level": "enabled","merge_requests_access_level": "enabled","forking_access_level": "enabled","wiki_access_level": "disabled","builds_access_level": "enabled","snippets_access_level": "enabled","pages_access_level": "private","operations_access_level": "disabled","analytics_access_level": "enabled","emails_disabled": null,"shared_runners_enabled": true,"lfs_enabled": false,"creator_id": 818,"import_status": "none","import_error": null,"open_issues_count": 0,"runners_token": "1234456","ci_default_git_depth": 50,"ci_forward_deployment_enabled": true,"public_jobs": true,"build_git_strategy": "fetch","build_timeout": 3600,"auto_cancel_pending_pipelines": "enabled","build_coverage_regex": null,"ci_config_path": "","shared_with_groups": [],"only_allow_merge_if_pipeline_succeeds": true,"allow_merge_on_skipped_pipeline": false,"restrict_user_defined_variables": false,"request_access_enabled": true,"only_allow_merge_if_all_discussions_are_resolved": true,"remove_source_branch_after_merge": true,"printing_merge_request_link_enabled": true,"merge_method": "merge","suggestion_commit_message": "","auto_devops_enabled": false,"auto_devops_deploy_strategy": "continuous","autoclose_referenced_issues": true,"repository_storage": "default","approvals_before_merge": 0,"mirror": false,"external_authorization_classification_label": null,"marked_for_deletion_at": null,"marked_for_deletion_on": null,"requirements_enabled": false,"compliance_frameworks": [],"permissions": {"project_access": null,"group_access": {"access_level": 50,"notification_level": 3}}}`
Expand Down

0 comments on commit 556de93

Please sign in to comment.