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  }