github.com/haya14busa/reviewdog@v0.0.0-20180723114510-ffb00ef78fd3/gitlab_mr_discussion.go (about) 1 package reviewdog 2 3 import ( 4 "context" 5 "fmt" 6 "net/url" 7 "path/filepath" 8 "sync" 9 10 gitlab "github.com/xanzy/go-gitlab" 11 "golang.org/x/sync/errgroup" 12 ) 13 14 // GitLabMergeRequestDiscussionCommenter is a comment and diff service for GitLab MergeRequest. 15 // 16 // API: 17 // https://docs.gitlab.com/ee/api/discussions.html#create-new-merge-request-discussion 18 // POST /projects/:id/merge_requests/:merge_request_iid/discussions 19 type GitLabMergeRequestDiscussionCommenter struct { 20 cli *gitlab.Client 21 pr int 22 sha string 23 projects string 24 25 muComments sync.Mutex 26 postComments []*Comment 27 28 // wd is working directory relative to root of repository. 29 wd string 30 } 31 32 // NewGitLabMergeRequestDiscussionCommenter returns a new GitLabMergeRequestDiscussionCommenter service. 33 // GitLabMergeRequestDiscussionCommenter service needs git command in $PATH. 34 func NewGitLabMergeRequestDiscussionCommenter(cli *gitlab.Client, owner, repo string, pr int, sha string) (*GitLabMergeRequestDiscussionCommenter, error) { 35 workDir, err := gitRelWorkdir() 36 if err != nil { 37 return nil, fmt.Errorf("GitLabMergeRequestDiscussionCommenter needs 'git' command: %v", err) 38 } 39 return &GitLabMergeRequestDiscussionCommenter{ 40 cli: cli, 41 pr: pr, 42 sha: sha, 43 projects: owner + "/" + repo, 44 wd: workDir, 45 }, nil 46 } 47 48 // Post accepts a comment and holds it. Flush method actually posts comments to 49 // GitLab in parallel. 50 func (g *GitLabMergeRequestDiscussionCommenter) Post(_ context.Context, c *Comment) error { 51 c.Path = filepath.Join(g.wd, c.Path) 52 g.muComments.Lock() 53 defer g.muComments.Unlock() 54 g.postComments = append(g.postComments, c) 55 return nil 56 } 57 58 // Flush posts comments which has not been posted yet. 59 func (g *GitLabMergeRequestDiscussionCommenter) Flush(ctx context.Context) error { 60 g.muComments.Lock() 61 defer g.muComments.Unlock() 62 postedcs, err := g.createPostedCommetns() 63 if err != nil { 64 return fmt.Errorf("failed to create posted comments: %v", err) 65 } 66 return g.postCommentsForEach(ctx, postedcs) 67 } 68 69 func (g *GitLabMergeRequestDiscussionCommenter) createPostedCommetns() (postedcomments, error) { 70 postedcs := make(postedcomments) 71 discussions, err := listAllMergeRequestDiscussion(g.cli, g.projects, g.pr, &ListMergeRequestDiscussionOptions{PerPage: 100}) 72 if err != nil { 73 return nil, fmt.Errorf("failed to list all merge request discussions: %v", err) 74 } 75 for _, d := range discussions { 76 for _, note := range d.Notes { 77 pos := note.Position 78 if pos == nil || pos.NewPath == "" || pos.NewLine == 0 || note.Body == "" { 79 continue 80 } 81 postedcs.AddPostedComment(pos.NewPath, pos.NewLine, note.Body) 82 } 83 } 84 return postedcs, nil 85 } 86 87 func (g *GitLabMergeRequestDiscussionCommenter) postCommentsForEach(ctx context.Context, postedcs postedcomments) error { 88 mr, _, err := g.cli.MergeRequests.GetMergeRequest(g.projects, g.pr, nil, gitlab.WithContext(ctx)) 89 if err != nil { 90 return fmt.Errorf("failed to get merge request: %v", err) 91 } 92 targetBranch, _, err := g.cli.Branches.GetBranch(mr.TargetProjectID, mr.TargetBranch, nil) 93 if err != nil { 94 return err 95 } 96 97 var eg errgroup.Group 98 for _, c := range g.postComments { 99 comment := c 100 if postedcs.IsPosted(comment, comment.Lnum) { 101 continue 102 } 103 eg.Go(func() error { 104 discussion := &GitLabMergeRequestDiscussion{ 105 Body: commentBody(comment), 106 Position: &GitLabMergeRequestDiscussionPosition{ 107 StartSHA: targetBranch.Commit.ID, 108 HeadSHA: g.sha, 109 BaseSHA: g.sha, 110 PositionType: "text", 111 NewPath: comment.Path, 112 NewLine: comment.Lnum, 113 }, 114 } 115 _, err := CreateMergeRequestDiscussion(g.cli, g.projects, g.pr, discussion) 116 return err 117 }) 118 } 119 return eg.Wait() 120 } 121 122 // GitLabMergeRequestDiscussionPosition represents position of GitLab MergeRequest Discussion. 123 type GitLabMergeRequestDiscussionPosition struct { 124 // Required. 125 BaseSHA string `json:"base_sha,omitempty"` // Base commit SHA in the source branch 126 StartSHA string `json:"start_sha,omitempty"` // SHA referencing commit in target branch 127 HeadSHA string `json:"head_sha,omitempty"` // SHA referencing HEAD of this merge request 128 PositionType string `json:"position_type,omitempty"` // Type of the position reference', allowed values: 'text' or 'image' 129 130 // Optional. 131 NewPath string `json:"new_path,omitempty"` // File path after change 132 NewLine int `json:"new_line,omitempty"` // Line number after change (for 'text' diff notes) 133 OldPath string `json:"old_path,omitempty"` // File path before change 134 OldLine int `json:"old_line,omitempty"` // Line number before change (for 'text' diff notes) 135 } 136 137 // GitLabMergeRequestDiscussionList represents response of ListMergeRequestDiscussion API. 138 // 139 // GitLab API docs: https://docs.gitlab.com/ee/api/discussions.html#list-project-merge-request-discussions 140 type GitLabMergeRequestDiscussionList struct { 141 Notes []*GitLabMergeRequestDiscussion `json:"notes"` 142 } 143 144 // GitLabMergeRequestDiscussion represents a discussion of MergeRequest. 145 type GitLabMergeRequestDiscussion struct { 146 Body string `json:"body"` // The content of a discussion 147 Position *GitLabMergeRequestDiscussionPosition `json:"position"` 148 } 149 150 // CreateMergeRequestDiscussion creates new discussion on a merge request. 151 // 152 // GitLab API docs: https://docs.gitlab.com/ee/api/discussions.html#create-new-merge-request-discussion 153 func CreateMergeRequestDiscussion(cli *gitlab.Client, projectID string, mergeRequest int, discussion *GitLabMergeRequestDiscussion) (*gitlab.Response, error) { 154 u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions", url.QueryEscape(projectID), mergeRequest) 155 req, err := cli.NewRequest("POST", u, discussion, nil) 156 if err != nil { 157 return nil, err 158 } 159 return cli.Do(req, nil) 160 } 161 162 // ListMergeRequestDiscussion lists discussion on a merge request. 163 // 164 // GitLab API docs: https://docs.gitlab.com/ee/api/discussions.html#list-project-merge-request-discussions 165 func ListMergeRequestDiscussion(cli *gitlab.Client, projectID string, mergeRequest int, opts *ListMergeRequestDiscussionOptions) ([]*GitLabMergeRequestDiscussionList, *gitlab.Response, error) { 166 u := fmt.Sprintf("projects/%s/merge_requests/%d/discussions", url.QueryEscape(projectID), mergeRequest) 167 req, err := cli.NewRequest("GET", u, opts, nil) 168 if err != nil { 169 return nil, nil, err 170 } 171 var discussions []*GitLabMergeRequestDiscussionList 172 resp, err := cli.Do(req, &discussions) 173 if err != nil { 174 return nil, resp, err 175 } 176 return discussions, resp, nil 177 } 178 179 // ListMergeRequestDiscussionOptions represents the available ListMergeRequestDiscussion() options. 180 // 181 // GitLab API docs: https://docs.gitlab.com/ee/api/discussions.html#list-project-merge-request-discussions 182 type ListMergeRequestDiscussionOptions gitlab.ListOptions 183 184 func listAllMergeRequestDiscussion(cli *gitlab.Client, projectID string, mergeRequest int, opts *ListMergeRequestDiscussionOptions) ([]*GitLabMergeRequestDiscussionList, error) { 185 discussions, resp, err := ListMergeRequestDiscussion(cli, projectID, mergeRequest, opts) 186 if err != nil { 187 return nil, err 188 } 189 if resp.NextPage == 0 { 190 return discussions, nil 191 } 192 newOpts := &ListMergeRequestDiscussionOptions{ 193 Page: resp.NextPage, 194 PerPage: opts.PerPage, 195 } 196 restDiscussions, err := listAllMergeRequestDiscussion(cli, projectID, mergeRequest, newOpts) 197 if err != nil { 198 return nil, err 199 } 200 return append(discussions, restDiscussions...), nil 201 }