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 }