github.com/haya14busa/reviewdog@v0.0.0-20180723114510-ffb00ef78fd3/github.go (about)

     1  package reviewdog
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"sync"
     8  
     9  	"github.com/google/go-github/github"
    10  )
    11  
    12  var _ CommentService = &GitHubPullRequest{}
    13  var _ DiffService = &GitHubPullRequest{}
    14  
    15  // GitHubPullRequest is a comment and diff service for GitHub PullRequest.
    16  //
    17  // API:
    18  //	https://developer.github.com/v3/pulls/comments/#create-a-comment
    19  //	POST /repos/:owner/:repo/pulls/:number/comments
    20  type GitHubPullRequest struct {
    21  	cli   *github.Client
    22  	owner string
    23  	repo  string
    24  	pr    int
    25  	sha   string
    26  
    27  	muComments   sync.Mutex
    28  	postComments []*Comment
    29  
    30  	postedcs postedcomments
    31  
    32  	// wd is working directory relative to root of repository.
    33  	wd string
    34  }
    35  
    36  // NewGitHubPullReqest returns a new GitHubPullRequest service.
    37  // GitHubPullRequest service needs git command in $PATH.
    38  func NewGitHubPullReqest(cli *github.Client, owner, repo string, pr int, sha string) (*GitHubPullRequest, error) {
    39  	workDir, err := gitRelWorkdir()
    40  	if err != nil {
    41  		return nil, fmt.Errorf("GitHubPullRequest needs 'git' command: %v", err)
    42  	}
    43  	return &GitHubPullRequest{
    44  		cli:   cli,
    45  		owner: owner,
    46  		repo:  repo,
    47  		pr:    pr,
    48  		sha:   sha,
    49  		wd:    workDir,
    50  	}, nil
    51  }
    52  
    53  // Post accepts a comment and holds it. Flush method actually posts comments to
    54  // GitHub in parallel.
    55  func (g *GitHubPullRequest) Post(_ context.Context, c *Comment) error {
    56  	c.Path = filepath.Join(g.wd, c.Path)
    57  	g.muComments.Lock()
    58  	defer g.muComments.Unlock()
    59  	g.postComments = append(g.postComments, c)
    60  	return nil
    61  }
    62  
    63  // Flush posts comments which has not been posted yet.
    64  func (g *GitHubPullRequest) Flush(ctx context.Context) error {
    65  	g.muComments.Lock()
    66  	defer g.muComments.Unlock()
    67  
    68  	if err := g.setPostedComment(ctx); err != nil {
    69  		return err
    70  	}
    71  	return g.postAsReviewComment(ctx)
    72  }
    73  
    74  func (g *GitHubPullRequest) postAsReviewComment(ctx context.Context) error {
    75  	comments := make([]*github.DraftReviewComment, 0, len(g.postComments))
    76  	for _, c := range g.postComments {
    77  		if g.postedcs.IsPosted(c, c.LnumDiff) {
    78  			continue
    79  		}
    80  		cbody := commentBody(c)
    81  		comments = append(comments, &github.DraftReviewComment{
    82  			Path:     &c.Path,
    83  			Position: &c.LnumDiff,
    84  			Body:     &cbody,
    85  		})
    86  	}
    87  
    88  	if len(comments) == 0 {
    89  		return nil
    90  	}
    91  
    92  	// TODO(haya14busa): it might be useful to report overview results by "body"
    93  	// field.
    94  	review := &github.PullRequestReviewRequest{
    95  		CommitID: &g.sha,
    96  		Event:    github.String("COMMENT"),
    97  		Comments: comments,
    98  	}
    99  	_, _, err := g.cli.PullRequests.CreateReview(ctx, g.owner, g.repo, g.pr, review)
   100  	return err
   101  }
   102  
   103  func (g *GitHubPullRequest) setPostedComment(ctx context.Context) error {
   104  	g.postedcs = make(postedcomments)
   105  	cs, err := g.comment(ctx)
   106  	if err != nil {
   107  		return err
   108  	}
   109  	for _, c := range cs {
   110  		if c.Position == nil || c.Path == nil || c.Body == nil {
   111  			// skip resolved comments. Or comments which do not have "path" nor
   112  			// "body".
   113  			continue
   114  		}
   115  		g.postedcs.AddPostedComment(c.GetPath(), c.GetPosition(), c.GetBody())
   116  	}
   117  	return nil
   118  }
   119  
   120  // Diff returns a diff of PullRequest.
   121  func (g *GitHubPullRequest) Diff(ctx context.Context) ([]byte, error) {
   122  	opt := github.RawOptions{Type: github.Diff}
   123  	d, _, err := g.cli.PullRequests.GetRaw(ctx, g.owner, g.repo, g.pr, opt)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	return []byte(d), nil
   128  }
   129  
   130  // Strip returns 1 as a strip of git diff.
   131  func (g *GitHubPullRequest) Strip() int {
   132  	return 1
   133  }
   134  
   135  func (g *GitHubPullRequest) comment(ctx context.Context) ([]*github.PullRequestComment, error) {
   136  	// https://developer.github.com/v3/guides/traversing-with-pagination/
   137  	opts := &github.PullRequestListCommentsOptions{
   138  		ListOptions: github.ListOptions{
   139  			PerPage: 100,
   140  		},
   141  	}
   142  	comments, err := listAllPullRequestsComments(ctx, g.cli, g.owner, g.repo, g.pr, opts)
   143  	if err != nil {
   144  		return nil, err
   145  	}
   146  	return comments, nil
   147  }
   148  
   149  func listAllPullRequestsComments(ctx context.Context, cli *github.Client,
   150  	owner, repo string, pr int, opts *github.PullRequestListCommentsOptions) ([]*github.PullRequestComment, error) {
   151  	comments, resp, err := cli.PullRequests.ListComments(ctx, owner, repo, pr, opts)
   152  	if err != nil {
   153  		return nil, err
   154  	}
   155  	if resp.NextPage == 0 {
   156  		return comments, nil
   157  	}
   158  	newOpts := &github.PullRequestListCommentsOptions{
   159  		ListOptions: github.ListOptions{
   160  			Page:    resp.NextPage,
   161  			PerPage: opts.PerPage,
   162  		},
   163  	}
   164  	restComments, err := listAllPullRequestsComments(ctx, cli, owner, repo, pr, newOpts)
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	return append(comments, restComments...), nil
   169  }