github.com/reviewdog/reviewdog@v0.17.5-0.20240516205324-0cd103a83d58/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/commentutil" 16 "github.com/reviewdog/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.Ptr(body), 98 Path: gitlab.Ptr(loc.GetPath()), 99 Line: gitlab.Ptr(lnum), 100 LineType: gitlab.Ptr("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 }