github.com/mattbailey/reviewdog@v0.10.0/service/gitlab/gitlab_mr_discussion.go (about) 1 package gitlab 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "sync" 8 9 "github.com/xanzy/go-gitlab" 10 "golang.org/x/sync/errgroup" 11 12 "github.com/reviewdog/reviewdog" 13 "github.com/reviewdog/reviewdog/service/serviceutil" 14 ) 15 16 // GitLabMergeRequestDiscussionCommenter is a comment and diff service for GitLab MergeRequest. 17 // 18 // API: 19 // https://docs.gitlab.com/ee/api/discussions.html#create-new-merge-request-discussion 20 // POST /projects/:id/merge_requests/:merge_request_iid/discussions 21 type GitLabMergeRequestDiscussionCommenter struct { 22 cli *gitlab.Client 23 pr int 24 sha string 25 projects string 26 27 muComments sync.Mutex 28 postComments []*reviewdog.Comment 29 30 // wd is working directory relative to root of repository. 31 wd string 32 } 33 34 // NewGitLabMergeRequestDiscussionCommenter returns a new GitLabMergeRequestDiscussionCommenter service. 35 // GitLabMergeRequestDiscussionCommenter service needs git command in $PATH. 36 func NewGitLabMergeRequestDiscussionCommenter(cli *gitlab.Client, owner, repo string, pr int, sha string) (*GitLabMergeRequestDiscussionCommenter, error) { 37 workDir, err := serviceutil.GitRelWorkdir() 38 if err != nil { 39 return nil, fmt.Errorf("GitLabMergeRequestDiscussionCommenter needs 'git' command: %v", err) 40 } 41 return &GitLabMergeRequestDiscussionCommenter{ 42 cli: cli, 43 pr: pr, 44 sha: sha, 45 projects: owner + "/" + repo, 46 wd: workDir, 47 }, nil 48 } 49 50 // Post accepts a comment and holds it. Flush method actually posts comments to 51 // GitLab in parallel. 52 func (g *GitLabMergeRequestDiscussionCommenter) Post(_ context.Context, c *reviewdog.Comment) error { 53 c.Path = filepath.Join(g.wd, c.Path) 54 g.muComments.Lock() 55 defer g.muComments.Unlock() 56 g.postComments = append(g.postComments, c) 57 return nil 58 } 59 60 // Flush posts comments which has not been posted yet. 61 func (g *GitLabMergeRequestDiscussionCommenter) Flush(ctx context.Context) error { 62 g.muComments.Lock() 63 defer g.muComments.Unlock() 64 postedcs, err := g.createPostedCommetns() 65 if err != nil { 66 return fmt.Errorf("failed to create posted comments: %v", err) 67 } 68 return g.postCommentsForEach(ctx, postedcs) 69 } 70 71 func (g *GitLabMergeRequestDiscussionCommenter) createPostedCommetns() (serviceutil.PostedComments, error) { 72 postedcs := make(serviceutil.PostedComments) 73 discussions, err := listAllMergeRequestDiscussion(g.cli, g.projects, g.pr, &gitlab.ListMergeRequestDiscussionsOptions{PerPage: 100}) 74 if err != nil { 75 return nil, fmt.Errorf("failed to list all merge request discussions: %v", err) 76 } 77 for _, d := range discussions { 78 for _, note := range d.Notes { 79 pos := note.Position 80 if pos == nil || pos.NewPath == "" || pos.NewLine == 0 || note.Body == "" { 81 continue 82 } 83 postedcs.AddPostedComment(pos.NewPath, pos.NewLine, note.Body) 84 } 85 } 86 return postedcs, nil 87 } 88 89 func (g *GitLabMergeRequestDiscussionCommenter) postCommentsForEach(ctx context.Context, postedcs serviceutil.PostedComments) error { 90 mr, _, err := g.cli.MergeRequests.GetMergeRequest(g.projects, g.pr, nil, gitlab.WithContext(ctx)) 91 if err != nil { 92 return fmt.Errorf("failed to get merge request: %v", err) 93 } 94 targetBranch, _, err := g.cli.Branches.GetBranch(mr.TargetProjectID, mr.TargetBranch, nil) 95 if err != nil { 96 return err 97 } 98 99 var eg errgroup.Group 100 for _, c := range g.postComments { 101 comment := c 102 if postedcs.IsPosted(comment, comment.Lnum) { 103 continue 104 } 105 eg.Go(func() error { 106 discussion := &gitlab.CreateMergeRequestDiscussionOptions{ 107 Body: gitlab.String(serviceutil.CommentBody(comment)), 108 Position: &gitlab.NotePosition{ 109 StartSHA: targetBranch.Commit.ID, 110 HeadSHA: g.sha, 111 BaseSHA: targetBranch.Commit.ID, 112 PositionType: "text", 113 NewPath: comment.Path, 114 NewLine: comment.Lnum, 115 }, 116 } 117 _, _, err := g.cli.Discussions.CreateMergeRequestDiscussion(g.projects, g.pr, discussion) 118 if err != nil { 119 return fmt.Errorf("failed to create merge request discussion: %v", err) 120 } 121 return nil 122 }) 123 } 124 return eg.Wait() 125 } 126 127 func listAllMergeRequestDiscussion(cli *gitlab.Client, projectID string, mergeRequest int, opts *gitlab.ListMergeRequestDiscussionsOptions) ([]*gitlab.Discussion, error) { 128 discussions, resp, err := cli.Discussions.ListMergeRequestDiscussions(projectID, mergeRequest, opts) 129 if err != nil { 130 return nil, err 131 } 132 if resp.NextPage == 0 { 133 return discussions, nil 134 } 135 newOpts := &gitlab.ListMergeRequestDiscussionsOptions{ 136 Page: resp.NextPage, 137 PerPage: opts.PerPage, 138 } 139 restDiscussions, err := listAllMergeRequestDiscussion(cli, projectID, mergeRequest, newOpts) 140 if err != nil { 141 return nil, err 142 } 143 return append(discussions, restDiscussions...), nil 144 }