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