github.com/vipcoin-gold/reviewdog@v1.0.2/service/bitbucket/annotator.go (about) 1 package bitbucket 2 3 import ( 4 "context" 5 "fmt" 6 "path/filepath" 7 "sync" 8 9 "github.com/vipcoin-gold/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 }