github.com/mattbailey/reviewdog@v0.10.0/service/gitlab/gitlab_mr_commit.go (about)

     1  package gitlab
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os/exec"
     7  	"path/filepath"
     8  	"strings"
     9  	"sync"
    10  
    11  	"github.com/xanzy/go-gitlab"
    12  	"golang.org/x/sync/errgroup"
    13  
    14  	"github.com/reviewdog/reviewdog"
    15  	"github.com/reviewdog/reviewdog/service/serviceutil"
    16  )
    17  
    18  var _ reviewdog.CommentService = &GitLabMergeRequestCommitCommenter{}
    19  
    20  // GitLabMergeRequestCommitCommenter is a comment service for GitLab MergeRequest.
    21  //
    22  // API:
    23  //  https://docs.gitlab.com/ce/api/commits.html#post-comment-to-commit
    24  //  POST /projects/:id/repository/commits/:sha/comments
    25  type GitLabMergeRequestCommitCommenter struct {
    26  	cli      *gitlab.Client
    27  	pr       int
    28  	sha      string
    29  	projects string
    30  
    31  	muComments   sync.Mutex
    32  	postComments []*reviewdog.Comment
    33  
    34  	postedcs serviceutil.PostedComments
    35  
    36  	// wd is working directory relative to root of repository.
    37  	wd string
    38  }
    39  
    40  // NewGitLabMergeRequestCommitCommenter returns a new GitLabMergeRequestCommitCommenter service.
    41  // GitLabMergeRequestCommitCommenter service needs git command in $PATH.
    42  func NewGitLabMergeRequestCommitCommenter(cli *gitlab.Client, owner, repo string, pr int, sha string) (*GitLabMergeRequestCommitCommenter, error) {
    43  	workDir, err := serviceutil.GitRelWorkdir()
    44  	if err != nil {
    45  		return nil, fmt.Errorf("GitLabMergeRequestCommitCommenter needs 'git' command: %v", err)
    46  	}
    47  	return &GitLabMergeRequestCommitCommenter{
    48  		cli:      cli,
    49  		pr:       pr,
    50  		sha:      sha,
    51  		projects: owner + "/" + repo,
    52  		wd:       workDir,
    53  	}, nil
    54  }
    55  
    56  // Post accepts a comment and holds it. Flush method actually posts comments to
    57  // GitLab in parallel.
    58  func (g *GitLabMergeRequestCommitCommenter) Post(_ context.Context, c *reviewdog.Comment) error {
    59  	c.Path = filepath.ToSlash(filepath.Join(g.wd, c.Path))
    60  	g.muComments.Lock()
    61  	defer g.muComments.Unlock()
    62  	g.postComments = append(g.postComments, c)
    63  	return nil
    64  }
    65  
    66  // Flush posts comments which has not been posted yet.
    67  func (g *GitLabMergeRequestCommitCommenter) Flush(ctx context.Context) error {
    68  	g.muComments.Lock()
    69  	defer g.muComments.Unlock()
    70  
    71  	if err := g.setPostedComment(ctx); err != nil {
    72  		return err
    73  	}
    74  
    75  	return g.postCommentsForEach(ctx)
    76  }
    77  
    78  func (g *GitLabMergeRequestCommitCommenter) postCommentsForEach(ctx context.Context) error {
    79  	var eg errgroup.Group
    80  	for _, c := range g.postComments {
    81  		comment := c
    82  		if g.postedcs.IsPosted(comment, comment.Lnum) {
    83  			continue
    84  		}
    85  		eg.Go(func() error {
    86  			commitID, err := g.getLastCommitsID(comment.Path, comment.Lnum)
    87  			if err != nil {
    88  				commitID = g.sha
    89  			}
    90  			body := serviceutil.CommentBody(comment)
    91  			ltype := "new"
    92  			prcomment := &gitlab.PostCommitCommentOptions{
    93  				Note:     &body,
    94  				Path:     &comment.Path,
    95  				Line:     &comment.Lnum,
    96  				LineType: &ltype,
    97  			}
    98  			_, _, err = g.cli.Commits.PostCommitComment(g.projects, commitID, prcomment, gitlab.WithContext(ctx))
    99  			return err
   100  		})
   101  	}
   102  	return eg.Wait()
   103  }
   104  
   105  func (g *GitLabMergeRequestCommitCommenter) getLastCommitsID(path string, line int) (string, error) {
   106  	lineFormat := fmt.Sprintf("%d,%d", line, line)
   107  	s, err := exec.Command("git", "blame", "-l", "-L", lineFormat, path).Output()
   108  	if err != nil {
   109  		return "", fmt.Errorf("failed to get commitID: %v", err)
   110  	}
   111  	commitID := strings.Split(string(s), " ")[0]
   112  	return commitID, nil
   113  }
   114  
   115  func (g *GitLabMergeRequestCommitCommenter) setPostedComment(ctx context.Context) error {
   116  	g.postedcs = make(serviceutil.PostedComments)
   117  	cs, err := g.comment(ctx)
   118  	if err != nil {
   119  		return err
   120  	}
   121  	for _, c := range cs {
   122  		if c.Line == 0 || c.Path == "" || c.Note == "" {
   123  			// skip resolved comments. Or comments which do not have "path" nor
   124  			// "body".
   125  			continue
   126  		}
   127  		g.postedcs.AddPostedComment(c.Path, c.Line, c.Note)
   128  	}
   129  	return nil
   130  }
   131  
   132  func (g *GitLabMergeRequestCommitCommenter) comment(ctx context.Context) ([]*gitlab.CommitComment, error) {
   133  	commits, _, err := g.cli.MergeRequests.GetMergeRequestCommits(
   134  		g.projects, g.pr, nil, gitlab.WithContext(ctx))
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  	comments := make([]*gitlab.CommitComment, 0)
   139  	for _, c := range commits {
   140  		tmpComments, _, err := g.cli.Commits.GetCommitComments(
   141  			g.projects, c.ID, nil, gitlab.WithContext(ctx))
   142  		if err != nil {
   143  			continue
   144  		}
   145  		comments = append(comments, tmpComments...)
   146  	}
   147  	return comments, nil
   148  }