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  			"![treemap](%s/api/v1/reports/%s/treemap/%s?base=%s)\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  }