github.com/covergates/covergates@v0.2.2-0.20201009050117-42ef8a19fb95/routers/api/report/report.go (about) 1 package report 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "strconv" 10 "strings" 11 12 "github.com/covergates/covergates/config" 13 "github.com/covergates/covergates/core" 14 "github.com/covergates/covergates/routers/api/request" 15 "github.com/gin-gonic/gin" 16 log "github.com/sirupsen/logrus" 17 ) 18 19 // HandleUpload report 20 // @Summary Upload coverage report 21 // @Tags Report 22 // @Param id path string true "report id" 23 // @Param file formData file true "report" 24 // @Param commit formData string true "Git commit SHA" 25 // @Param type formData string true "report type" 26 // @Param ref formData string false "ref" 27 // @Param root formData string false "git worktree root path" 28 // @Param files formData string false "files list of the repository" 29 // @Success 200 {string} string "ok" 30 // @Failure 400 {string} string "error message" 31 // @Router /reports/{id} [post] 32 func HandleUpload( 33 coverageService core.CoverageService, 34 reportStore core.ReportStore, 35 ) gin.HandlerFunc { 36 return func(c *gin.Context) { 37 if _, ok := c.GetPostForm("type"); !ok { 38 c.String(400, "must have report type") 39 return 40 } 41 42 if _, ok := c.GetPostForm("commit"); !ok { 43 c.String(400, "must have commit SHA") 44 return 45 } 46 47 reportID := c.Param("id") 48 ref := c.PostForm("ref") 49 reportType := core.ReportType(c.PostForm("type")) 50 commit := c.PostForm("commit") 51 root := c.PostForm("root") 52 53 ctx := c.Request.Context() 54 55 // get upload file 56 file, err := c.FormFile("file") 57 if err != nil { 58 c.Error(err) 59 c.String(400, err.Error()) 60 return 61 } 62 63 files := make([]string, 0) 64 if c.PostForm("files") != "" { 65 if err := json.Unmarshal([]byte(c.PostForm("files")), &files); err != nil { 66 c.Error(err) 67 c.String(500, err.Error()) 68 return 69 } 70 } 71 72 reader, err := file.Open() 73 coverage, err := loadCoverageReport( 74 ctx, 75 coverageService, 76 reportType, 77 reader, 78 root, 79 MustGetSetting(c), 80 ) 81 if err != nil { 82 log.Error(err) 83 c.String(500, err.Error()) 84 return 85 } 86 87 report := &core.Report{ 88 ReportID: reportID, 89 Coverages: []*core.CoverageReport{ 90 coverage, 91 }, 92 Files: files, 93 Reference: ref, 94 Commit: commit, 95 } 96 if err := reportStore.Upload(report); err != nil { 97 c.Error(err) 98 c.String(500, err.Error()) 99 return 100 } 101 c.String(200, "ok") 102 } 103 } 104 105 // HandleRepo for report id 106 // @Summary get repository of the report id 107 // @Tags Report 108 // @Param id path string true "report id" 109 // @Success 200 {object} core.Repo "repository" 110 // @Failure 400 {string} string "error message" 111 // @Router /reports/{id}/repo [get] 112 func HandleRepo(store core.RepoStore) gin.HandlerFunc { 113 return func(c *gin.Context) { 114 repo, err := store.Find(&core.Repo{ 115 ReportID: c.Param("id"), 116 }) 117 if err != nil { 118 c.JSON(404, &core.Repo{}) 119 return 120 } 121 c.JSON(200, repo) 122 } 123 } 124 125 type getOptions struct { 126 Latest bool `form:"latest"` 127 Ref string `form:"ref"` 128 } 129 130 // HandleGet for the report id 131 // @Summary get reports for the report id 132 // @Tags Report 133 // @Param id path string true "report id" 134 // @Param latest query bool false "get only the latest report" 135 // @Param ref query string false "get report for git ref" 136 // @Success 200 {object} core.Report "coverage report" 137 // @Router /reports/{id} [get] 138 func HandleGet( 139 reportStore core.ReportStore, 140 repoStore core.RepoStore, 141 service core.SCMService, 142 ) gin.HandlerFunc { 143 return func(c *gin.Context) { 144 reportID := c.Param("id") 145 option := &getOptions{} 146 if err := c.BindQuery(option); err != nil { 147 log.Error(err) 148 c.JSON(400, []*core.Report{}) 149 return 150 } 151 if !hasPermission(c, repoStore, service, reportID) { 152 c.JSON(401, []*core.Report{}) 153 return 154 } 155 // TODO: support multiple type (language) reports in one repository 156 var err error 157 var reports []*core.Report 158 if option.Latest && option.Ref == "" { 159 var report *core.Report 160 if report, err = getLatest(reportStore, repoStore, reportID); err == nil { 161 reports = []*core.Report{report} 162 } 163 } else if option.Latest && option.Ref != "" { 164 var report *core.Report 165 if report, err = getRef(reportStore, reportID, option.Ref); err == nil { 166 reports = []*core.Report{report} 167 } 168 } else if option.Ref != "" { 169 reports, err = reportStore.List(reportID, option.Ref) 170 } else { 171 reports, err = getAll(reportStore, reportID) 172 } 173 if err != nil { 174 c.Error(err) 175 c.JSON(404, []*core.Report{}) 176 return 177 } 178 c.JSON(200, reports) 179 } 180 } 181 182 // HandleGetTreeMap for coverage difference with main branch 183 // @Summary Get coverage difference treemap with main branch 184 // @Tags Report 185 // @Produce image/svg+xml 186 // @Param id path string true "report id" 187 // @param source path string true "source branch" 188 // @Success 200 {object} string "treemap svg" 189 // @Router /reports/{id}/treemap/{ref} [get] 190 func HandleGetTreeMap( 191 reportStore core.ReportStore, 192 repoStore core.RepoStore, 193 chartService core.ChartService, 194 ) gin.HandlerFunc { 195 return func(c *gin.Context) { 196 reportID := c.Param("id") 197 ref := strings.Trim(c.Param("ref"), "/") 198 new, err := getRef(reportStore, reportID, ref) 199 if err != nil { 200 c.String(500, err.Error()) 201 return 202 } 203 old, err := getLatest(reportStore, repoStore, reportID) 204 if err != nil { 205 old = &core.Report{ 206 Coverages: []*core.CoverageReport{}, 207 } 208 } 209 chart := chartService.CoverageDiffTreeMap(old, new) 210 buffer := bytes.NewBuffer([]byte{}) 211 if err := chart.Render(buffer); err != nil { 212 c.String(500, err.Error()) 213 return 214 } 215 c.Header("Cache-Control", "max-age=600") 216 c.Data(200, "image/svg+xml", buffer.Bytes()) 217 return 218 } 219 } 220 221 // HandleGetCard of the repository status 222 // @Summary Get status card of the repository 223 // @Tags Report 224 // @Produce image/svg+xml 225 // @Param id path string true "report id" 226 // @Success 200 {object} string "treemap svg" 227 // @Router /reports/{id}/card [get] 228 func HandleGetCard( 229 repoStore core.RepoStore, 230 reportStore core.ReportStore, 231 chartService core.ChartService, 232 ) gin.HandlerFunc { 233 return func(c *gin.Context) { 234 reportID := c.Param("id") 235 repo, err := repoStore.Find(&core.Repo{ReportID: reportID}) 236 if err != nil { 237 c.String(404, "repository not found") 238 return 239 } 240 report, err := reportStore.Find(&core.Report{ReportID: reportID, Reference: repo.Branch}) 241 if err != nil { 242 c.String(404, "report not found") 243 return 244 } 245 chart := chartService.RepoCard(repo, report) 246 buffer := bytes.NewBuffer([]byte{}) 247 if err := chart.Render(buffer); err != nil { 248 c.String(500, err.Error()) 249 return 250 } 251 c.Header("Cache-Control", "max-age=600") 252 c.Data(200, "image/svg+xml", buffer.Bytes()) 253 return 254 } 255 } 256 257 // HandleComment report summary 258 // @Summary Leave a report summary comment on pull request 259 // @Tags Report 260 // @Param id path string true "report id" 261 // @param number path string true "pull request number" 262 // @Success 200 {object} string "ok" 263 // @Router /reports/{id}/comment/{number} [POST] 264 func HandleComment( 265 config *config.Config, 266 service core.SCMService, 267 repoStore core.RepoStore, 268 reportStore core.ReportStore, 269 reportService core.ReportService, 270 ) gin.HandlerFunc { 271 // TODO: Add handle comment unit test 272 // TODO: Need to test comment with SHA or branch 273 return func(c *gin.Context) { 274 ctx := c.Request.Context() 275 reportID := c.Param("id") 276 number, err := strconv.Atoi(c.Param("number")) 277 if err != nil { 278 c.String(400, "invalid pull request number") 279 return 280 } 281 repo, err := repoStore.Find(&core.Repo{ReportID: reportID}) 282 if err != nil { 283 c.String(400, "repository not found") 284 return 285 } 286 user, err := repoStore.Creator(repo) 287 if err != nil { 288 c.String(400, "user not found") 289 return 290 } 291 client, err := service.Client(repo.SCM) 292 pr, err := client.PullRequests().Find(ctx, user, repo.FullName(), number) 293 if err != nil { 294 c.String(400, "cannot find pull request") 295 return 296 } 297 298 // TODO: handle multiple language repository 299 source, err := reportStore.Find(&core.Report{ReportID: reportID, Commit: pr.Commit}) 300 if err != nil { 301 if source, err = reportStore.Find(&core.Report{ReportID: reportID, Reference: pr.Source}); err != nil { 302 c.String(500, err.Error()) 303 return 304 } 305 } 306 target, err := reportStore.Find(&core.Report{ReportID: reportID, Reference: pr.Target}) 307 if err != nil { 308 target = &core.Report{} 309 } 310 311 r, err := reportService.MarkdownReport(source, target) 312 if err != nil { 313 c.String(500, err.Error()) 314 return 315 } 316 317 buf := &bytes.Buffer{} 318 319 buf.WriteString(fmt.Sprintf( 320 "\n\n", 321 config.Server.URL(), 322 reportID, 323 source.Reference, 324 target.Reference, 325 )) 326 327 if _, err := io.Copy(buf, r); err != nil { 328 c.String(500, err.Error()) 329 return 330 } 331 332 if comment, err := reportStore.FindComment(&core.Report{ReportID: reportID}, number); err == nil { 333 client.PullRequests().RemoveComment(ctx, user, repo.FullName(), number, comment.Comment) 334 } 335 336 commentID, err := client.PullRequests().CreateComment( 337 ctx, 338 user, 339 repo.FullName(), 340 number, 341 string(buf.Bytes()), 342 ) 343 log.Println(commentID) 344 if err != nil { 345 c.String(500, err.Error()) 346 return 347 } 348 comment := &core.ReportComment{ 349 Comment: commentID, 350 Number: number, 351 } 352 if err := reportStore.CreateComment(&core.Report{ReportID: reportID}, comment); err != nil { 353 c.String(500, err.Error()) 354 return 355 } 356 c.String(200, "ok") 357 } 358 } 359 360 func hasPermission( 361 c *gin.Context, 362 store core.RepoStore, 363 service core.SCMService, 364 reportID string, 365 ) bool { 366 repo, err := store.Find(&core.Repo{ReportID: reportID}) 367 if err != nil { 368 return false 369 } 370 if !repo.Private { 371 return true 372 } 373 user, ok := request.UserFrom(c) 374 if !ok { 375 return false 376 } 377 client, err := service.Client(repo.SCM) 378 if err != nil { 379 return false 380 } 381 _, err = client.Repositories().Find(c.Request.Context(), user, repo.FullName()) 382 if err != nil { 383 return false 384 } 385 return true 386 } 387 388 func getLatest(reportStore core.ReportStore, repoStore core.RepoStore, reportID string) (*core.Report, error) { 389 repo, err := repoStore.Find(&core.Repo{ReportID: reportID}) 390 if err != nil { 391 return nil, err 392 } 393 return reportStore.Find(&core.Report{ 394 ReportID: reportID, 395 Reference: repo.Branch, 396 }) 397 } 398 399 func getRef(store core.ReportStore, reportID, ref string) (*core.Report, error) { 400 var report *core.Report 401 var err error 402 seed := &core.Report{ReportID: reportID, Commit: ref} 403 if report, err = store.Find(seed); err == nil { 404 return report, err 405 } 406 seed = &core.Report{ReportID: reportID, Reference: ref} 407 if report, err = store.Find(seed); err == nil { 408 return report, err 409 } 410 return nil, err 411 } 412 413 // getAll reports related to given reportID 414 func getAll(store core.ReportStore, reportID string) ([]*core.Report, error) { 415 return store.Finds(&core.Report{ 416 ReportID: reportID, 417 }) 418 } 419 420 // loadCoverageReort from io reader and apply repository wide setting 421 func loadCoverageReport( 422 ctx context.Context, 423 service core.CoverageService, 424 reportType core.ReportType, 425 data io.Reader, 426 root string, 427 setting *core.RepoSetting, 428 ) (*core.CoverageReport, error) { 429 coverage, err := service.Report(ctx, reportType, data) 430 if err != nil { 431 return nil, err 432 } 433 if err := service.TrimFileNamePrefix(ctx, coverage, root); err != nil { 434 return nil, err 435 } 436 if err := service.TrimFileNames(ctx, coverage, setting.Filters); err != nil { 437 return nil, err 438 } 439 coverage.Type = reportType 440 return coverage, nil 441 } 442 443 // getGitRepository with given Repo 444 func getGitRepository( 445 ctx context.Context, 446 store core.RepoStore, 447 service core.SCMService, 448 repo *core.Repo, 449 ) (core.GitRepository, error) { 450 user, err := store.Creator(repo) 451 if err != nil { 452 return nil, err 453 } 454 client, err := service.Client(repo.SCM) 455 if err != nil { 456 return nil, err 457 } 458 return client.Git().GitRepository(ctx, user, repo.FullName()) 459 }