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  }