github.com/massongit/reviewdog@v0.0.0-20240331071725-4a16675475a8/service/gitea/gitea.go (about) 1 package gitea 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "sync" 8 9 "github.com/reviewdog/reviewdog" 10 "github.com/reviewdog/reviewdog/service/commentutil" 11 "github.com/reviewdog/reviewdog/service/serviceutil" 12 13 "code.gitea.io/sdk/gitea" 14 ) 15 16 var ( 17 _ reviewdog.CommentService = &PullRequest{} 18 _ reviewdog.DiffService = &PullRequest{} 19 ) 20 21 // PullRequest is a comment and diff service for Gitea PullRequest. 22 // 23 // API: 24 // 25 // https://try.gitea.io/api/swagger#/issue/issueCreateComment 26 // POST /repos/:owner/:repo/issues/:number/comments 27 type PullRequest struct { 28 cli *gitea.Client 29 owner string 30 repo string 31 pr int64 32 sha string 33 34 muComments sync.Mutex 35 postComments []*reviewdog.Comment 36 37 postedcs commentutil.PostedComments 38 39 // wd is working directory relative to root of repository. 40 wd string 41 } 42 43 // NewGiteaPullRequest returns a new PullRequest service. 44 // PullRequest service needs git command in $PATH. 45 func NewGiteaPullRequest(cli *gitea.Client, owner, repo string, pr int64, sha string) (*PullRequest, error) { 46 workDir, err := serviceutil.GitRelWorkdir() 47 if err != nil { 48 return nil, fmt.Errorf("pull request needs 'git' command: %w", err) 49 } 50 return &PullRequest{ 51 cli: cli, 52 owner: owner, 53 repo: repo, 54 pr: pr, 55 sha: sha, 56 wd: workDir, 57 }, nil 58 } 59 60 // Diff returns a diff of PullRequest. 61 func (g *PullRequest) Diff(_ context.Context) ([]byte, error) { 62 diff, _, err := g.cli.GetPullRequestDiff(g.owner, g.repo, g.pr, gitea.PullRequestDiffOptions{ 63 Binary: false, 64 }) 65 if err != nil { 66 return nil, err 67 } 68 69 return diff, nil 70 } 71 72 // Strip returns 1 as a strip of git diff. 73 func (g *PullRequest) Strip() int { 74 return 1 75 } 76 77 // Post accepts a comment and holds it. Flush method actually posts comments to 78 // Gitea in parallel. 79 func (g *PullRequest) Post(_ context.Context, c *reviewdog.Comment) error { 80 c.Result.Diagnostic.GetLocation().Path = filepath.ToSlash(filepath.Join(g.wd, 81 c.Result.Diagnostic.GetLocation().GetPath())) 82 83 g.muComments.Lock() 84 defer g.muComments.Unlock() 85 86 g.postComments = append(g.postComments, c) 87 88 return nil 89 } 90 91 // Flush posts comments which has not been posted yet. 92 func (g *PullRequest) Flush(_ context.Context) error { 93 g.muComments.Lock() 94 defer g.muComments.Unlock() 95 96 if err := g.setPostedComment(); err != nil { 97 return err 98 } 99 return g.postAsReviewComment() 100 } 101 102 // setPostedComment get posted comments from Gitea. 103 func (g *PullRequest) setPostedComment() error { 104 g.postedcs = make(commentutil.PostedComments) 105 106 cs, err := g.comment() 107 if err != nil { 108 return err 109 } 110 111 for _, c := range cs { 112 if c.LineNum == 0 || c.Path == "" || c.Body == "" { 113 continue 114 } 115 g.postedcs.AddPostedComment(c.Path, int(c.LineNum), c.Body) 116 } 117 118 return nil 119 } 120 121 func (g *PullRequest) comment() ([]*gitea.PullReviewComment, error) { 122 prs, err := listAllPullRequestReviews(g.cli, g.owner, g.repo, g.pr, gitea.ListPullReviewsOptions{ 123 ListOptions: gitea.ListOptions{ 124 Page: 1, 125 PageSize: 100, 126 }, 127 }) 128 if err != nil { 129 return nil, err 130 } 131 132 comments := make([]*gitea.PullReviewComment, 0, len(prs)) 133 for _, pr := range prs { 134 c, _, err := g.cli.ListPullReviewComments(g.owner, g.repo, g.pr, pr.ID) 135 if err != nil { 136 return nil, err 137 } 138 139 comments = append(comments, c...) 140 } 141 142 return comments, nil 143 } 144 145 func listAllPullRequestReviews(cli *gitea.Client, 146 owner, repo string, pr int64, opts gitea.ListPullReviewsOptions, 147 ) ([]*gitea.PullReview, error) { 148 prs, resp, err := cli.ListPullReviews(owner, repo, pr, opts) 149 if err != nil { 150 return nil, err 151 } 152 153 if resp.NextPage == 0 { 154 return prs, nil 155 } 156 157 newOpts := gitea.ListPullReviewsOptions{ 158 ListOptions: gitea.ListOptions{ 159 Page: resp.NextPage, 160 PageSize: opts.PageSize, 161 }, 162 } 163 164 restPrs, err := listAllPullRequestReviews(cli, owner, repo, pr, newOpts) 165 if err != nil { 166 return nil, err 167 } 168 169 return append(prs, restPrs...), nil 170 } 171 172 func (g *PullRequest) postAsReviewComment() error { 173 postComments := g.postComments 174 g.postComments = nil 175 reviewComments := make([]gitea.CreatePullReviewComment, 0, len(postComments)) 176 177 for _, comment := range postComments { 178 if !comment.Result.InDiffFile { 179 continue 180 } 181 182 body := commentutil.MarkdownComment(comment) 183 if g.postedcs.IsPosted(comment, giteaCommentLine(comment), body) { 184 // it's already posted. skip it. 185 continue 186 } 187 188 if !comment.Result.InDiffContext { 189 // If the result is outside of diff context, skip it. 190 continue 191 } 192 193 reviewComments = append(reviewComments, buildReviewComment(comment, body)) 194 } 195 196 if len(reviewComments) > 0 { 197 // send review comments to Gitea. 198 review := gitea.CreatePullReviewOptions{ 199 CommitID: g.sha, 200 State: gitea.ReviewStateComment, 201 Comments: reviewComments, 202 } 203 _, _, err := g.cli.CreatePullReview(g.owner, g.repo, g.pr, review) 204 if err != nil { 205 return err 206 } 207 } 208 209 return nil 210 } 211 212 func buildReviewComment(c *reviewdog.Comment, body string) gitea.CreatePullReviewComment { 213 loc := c.Result.Diagnostic.GetLocation() 214 215 return gitea.CreatePullReviewComment{ 216 Body: body, 217 Path: loc.GetPath(), 218 NewLineNum: int64(giteaCommentLine(c)), 219 } 220 } 221 222 // line represents end line if it's a multiline comment in Gitea, otherwise 223 // it's start line. 224 func giteaCommentLine(c *reviewdog.Comment) int { 225 if !c.Result.InDiffContext { 226 return 0 227 } 228 229 loc := c.Result.Diagnostic.GetLocation() 230 startLine := loc.GetRange().GetStart().GetLine() 231 endLine := loc.GetRange().GetEnd().GetLine() 232 233 if endLine == 0 { 234 endLine = startLine 235 } 236 237 return int(endLine) 238 }