github.com/haya14busa/reviewdog@v0.0.0-20180723114510-ffb00ef78fd3/gitlab_mr_commit.go (about)

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