github.com/reviewdog/reviewdog@v0.17.5-0.20240516205324-0cd103a83d58/service/bitbucket/annotator.go (about)

     1  package bitbucket
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"sync"
     8  
     9  	"github.com/reviewdog/reviewdog"
    10  )
    11  
    12  var _ reviewdog.CommentService = &ReportAnnotator{}
    13  
    14  const (
    15  	// avatar from https://github.com/apps/reviewdog
    16  	logoURL  = "https://avatars1.githubusercontent.com/in/12131"
    17  	reporter = "reviewdog"
    18  	// max amount of annotations in one batch call
    19  	annotationsBatchSize = 100
    20  )
    21  
    22  // ReportAnnotator is a comment service for Bitbucket Code Insights reports.
    23  //
    24  // Cloud API:
    25  //
    26  //	https://developer.atlassian.com/bitbucket/api/2/reference/resource/repositories/%7Bworkspace%7D/%7Brepo_slug%7D/commit/%7Bcommit%7D/reports/%7BreportId%7D/annotations#post
    27  //	POST /2.0/repositories/{username}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations
    28  //
    29  // Server API:
    30  //
    31  //	https://docs.atlassian.com/bitbucket-server/rest/5.15.0/bitbucket-code-insights-rest.html#idm288218233536
    32  //	/rest/insights/1.0/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/reports/{key}/annotations
    33  type ReportAnnotator struct {
    34  	cli         APIClient
    35  	sha         string
    36  	owner, repo string
    37  
    38  	muAnnotations sync.Mutex
    39  	// store annotations in map per tool name
    40  	// so we can create report per tool
    41  	comments map[string][]*reviewdog.Comment
    42  
    43  	// wd is working directory relative to root of repository.
    44  	wd         string
    45  	duplicates map[string]struct{}
    46  }
    47  
    48  // NewReportAnnotator creates new Bitbucket ReportRequest Annotator
    49  func NewReportAnnotator(cli APIClient, owner, repo, sha string, runners []string) *ReportAnnotator {
    50  	r := &ReportAnnotator{
    51  		cli:        cli,
    52  		sha:        sha,
    53  		owner:      owner,
    54  		repo:       repo,
    55  		comments:   make(map[string][]*reviewdog.Comment, len(runners)),
    56  		duplicates: map[string]struct{}{},
    57  	}
    58  
    59  	// pre populate map of annotations, so we still create passed (green) report
    60  	// if no issues found from the specific tool
    61  	for _, runner := range runners {
    62  		if len(runner) == 0 {
    63  			continue
    64  		}
    65  		r.comments[runner] = []*reviewdog.Comment{}
    66  		// create Pending report for each tool
    67  		_ = r.createOrUpdateReport(
    68  			context.Background(),
    69  			reportID(runner, reporter),
    70  			reportTitle(runner, reporter),
    71  			reportResultPending,
    72  		)
    73  	}
    74  
    75  	return r
    76  }
    77  
    78  // Post accepts a comment and holds it. Flush method actually posts comments to
    79  // Bitbucket in batch.
    80  func (r *ReportAnnotator) Post(_ context.Context, c *reviewdog.Comment) error {
    81  	c.Result.Diagnostic.GetLocation().Path = filepath.ToSlash(
    82  		filepath.Join(r.wd, c.Result.Diagnostic.GetLocation().GetPath()))
    83  
    84  	r.muAnnotations.Lock()
    85  	defer r.muAnnotations.Unlock()
    86  
    87  	// deduplicate event, because some reporters might report
    88  	// it twice, and bitbucket api will complain on duplicated
    89  	// external id of annotation
    90  	commentID := externalIDFromDiagnostic(c.Result.Diagnostic)
    91  	if _, exist := r.duplicates[commentID]; !exist {
    92  		r.comments[c.ToolName] = append(r.comments[c.ToolName], c)
    93  		r.duplicates[commentID] = struct{}{}
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  // Flush posts comments which has not been posted yet.
   100  func (r *ReportAnnotator) Flush(ctx context.Context) error {
   101  	r.muAnnotations.Lock()
   102  	defer r.muAnnotations.Unlock()
   103  
   104  	// create/update/annotate report per tool
   105  	for tool, comments := range r.comments {
   106  		reportID := reportID(tool, reporter)
   107  		title := reportTitle(tool, reporter)
   108  		if len(comments) == 0 {
   109  			// if no annotation, create Passed report
   110  			if err := r.createOrUpdateReport(ctx, reportID, title, reportResultPassed); err != nil {
   111  				return err
   112  			}
   113  			// and move one
   114  			continue
   115  		}
   116  
   117  		// create report or update report first, with the failed status
   118  		if err := r.createOrUpdateReport(ctx, reportID, title, reportResultFailed); err != nil {
   119  			return err
   120  		}
   121  
   122  		// send comments in batches, because of the api max payload size limit
   123  		for start, annCount := 0, len(comments); start < annCount; start += annotationsBatchSize {
   124  			end := start + annotationsBatchSize
   125  
   126  			if end > annCount {
   127  				end = annCount
   128  			}
   129  
   130  			req := &AnnotationsRequest{
   131  				Owner:      r.owner,
   132  				Repository: r.repo,
   133  				Commit:     r.sha,
   134  				ReportID:   reportID,
   135  				Comments:   comments[start:end],
   136  			}
   137  
   138  			err := r.cli.CreateOrUpdateAnnotations(ctx, req)
   139  			if err != nil {
   140  				return fmt.Errorf("failed to post annotations: %w", err)
   141  			}
   142  		}
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (r *ReportAnnotator) createOrUpdateReport(ctx context.Context, id, title, reportStatus string) error {
   149  	req := &ReportRequest{
   150  		ReportID:   id,
   151  		Owner:      r.owner,
   152  		Repository: r.repo,
   153  		Commit:     r.sha,
   154  		Type:       reportTypeBug,
   155  		Title:      title,
   156  		Reporter:   reporter,
   157  		Result:     reportStatus,
   158  		LogoURL:    logoURL,
   159  	}
   160  
   161  	switch reportStatus {
   162  	case reportResultPassed:
   163  		req.Details = "Great news! Reviewdog couldn't spot any issues!"
   164  	case reportResultPending:
   165  		req.Details = "Please wait for Reviewdog to finish checking your code for issues."
   166  	default:
   167  		req.Details = "Woof-Woof! This report generated for you by reviewdog."
   168  	}
   169  
   170  	return r.cli.CreateOrUpdateReport(ctx, req)
   171  }