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  }