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