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

     1  package main
     2  
     3  import (
     4  	"crypto/tls"
     5  	"errors"
     6  	"flag"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"os/exec"
    14  	"sort"
    15  	"text/tabwriter"
    16  
    17  	"golang.org/x/net/context" // "context"
    18  
    19  	"golang.org/x/oauth2"
    20  
    21  	"github.com/google/go-github/github"
    22  	"github.com/haya14busa/errorformat/fmts"
    23  	"github.com/haya14busa/reviewdog"
    24  	"github.com/haya14busa/reviewdog/cienv"
    25  	"github.com/haya14busa/reviewdog/project"
    26  	shellwords "github.com/mattn/go-shellwords"
    27  	"github.com/xanzy/go-gitlab"
    28  )
    29  
    30  const usageMessage = "" +
    31  	`Usage:	reviewdog [flags]
    32  	reviewdog accepts any compiler or linter results from stdin and filters
    33  	them by diff for review. reviewdog also can posts the results as a comment to
    34  	GitHub if you use reviewdog in CI service.
    35  `
    36  
    37  type option struct {
    38  	version   bool
    39  	diffCmd   string
    40  	diffStrip int
    41  	efms      strslice
    42  	f         string // errorformat name
    43  	list      bool   // list supported errorformat name
    44  	name      string // tool name which is used in comment
    45  	ci        string
    46  	conf      string
    47  	reporter  string
    48  }
    49  
    50  // flags doc
    51  const (
    52  	diffCmdDoc   = `diff command (e.g. "git diff"). diff flag is ignored if you pass "ci" flag`
    53  	diffStripDoc = "strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)"
    54  	efmsDoc      = `list of errorformat (https://github.com/haya14busa/errorformat)`
    55  	fDoc         = `format name (run -list to see supported format name) for input. It's also used as tool name in review comment if -name is empty`
    56  	listDoc      = `list supported pre-defined format names which can be used as -f arg`
    57  	nameDoc      = `tool name in review comment. -f is used as tool name if -name is empty`
    58  	ciDoc        = `[deprecated] reviewdog automatically get necessary data. See also -reporter for migration`
    59  	confDoc      = `config file path`
    60  	reporterDoc  = `reporter of reviewdog results. (local, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit)
    61  	"local" (default)
    62  		Report results to stdout.
    63  
    64  	"github-pr-check" (experimental)
    65  		Report results to GitHub PullRequest Check tab.
    66  
    67  		1. Install reviedog Apps. https://github.com/apps/reviewdog
    68  		2. Set REVIEWDOG_TOKEN or run reviewdog CLI in trusted CI providers.
    69  		You can get token from https://reviewdog.app/gh/<owner>/<repo-name>.
    70  		$ export REVIEWDOG_TOKEN="xxxxx"
    71  
    72  		Note: Token is not required if you run reviewdog in Travis CI.
    73  
    74  	"github-pr-review"
    75  		Report results to GitHub review comments.
    76  
    77  		1. Set REVIEWDOG_GITHUB_API_TOKEN environment variable.
    78  		Go to https://github.com/settings/tokens and create new Personal access token with repo scope.
    79  
    80  		For GitHub Enterprise:
    81  			$ export GITHUB_API="https://example.githubenterprise.com/api/v3"
    82  
    83  	"gitlab-mr-discussion"
    84  		Report results to GitLab MergeRequest discussion.
    85  
    86  		1. Set REVIEWDOG_GITLAB_API_TOKEN environment variable.
    87  		Go to https://gitlab.com/profile/personal_access_tokens
    88  
    89  		For self hosted GitLab:
    90  			$ export GITLAB_API="https://example.gitlab.com/api/v4"
    91  
    92  	"gitlab-mr-commit"
    93  		Same as gitlab-mr-discussion, but report results to GitLab comments for
    94  		each commits in Merge Requests.
    95  
    96  	For GitHub Enterprise and self hosted GitLab, set
    97  	REVIEWDOG_INSECURE_SKIP_VERIFY to skip verifying SSL (please use this at your own risk)
    98  		$ export REVIEWDOG_INSECURE_SKIP_VERIFY=true
    99  
   100  	For non-local reporters, reviewdog automatically get necessary data from
   101  	environment variable in CI service (Travis CI, Circle CI, dronel.io, GitLab CI).
   102  	You can set necessary data with following environment variable manually if
   103  	you want (e.g. run reviewdog in Jenkins).
   104  
   105  		$ export CI_PULL_REQUEST=14 # Pull Request number (e.g. 14)
   106  		$ export CI_COMMIT="$(git rev-parse @)" # SHA1 for the current build
   107  		$ export CI_REPO_OWNER="haya14busa" # repository owner
   108  		$ export CI_REPO_NAME="reviewdog" # repository name
   109  `
   110  )
   111  
   112  var opt = &option{}
   113  
   114  func init() {
   115  	flag.BoolVar(&opt.version, "version", false, "print version")
   116  	flag.StringVar(&opt.diffCmd, "diff", "", diffCmdDoc)
   117  	flag.IntVar(&opt.diffStrip, "strip", 1, diffStripDoc)
   118  	flag.Var(&opt.efms, "efm", efmsDoc)
   119  	flag.StringVar(&opt.f, "f", "", fDoc)
   120  	flag.BoolVar(&opt.list, "list", false, listDoc)
   121  	flag.StringVar(&opt.name, "name", "", nameDoc)
   122  	flag.StringVar(&opt.ci, "ci", "", ciDoc)
   123  	flag.StringVar(&opt.conf, "conf", "", confDoc)
   124  	flag.StringVar(&opt.reporter, "reporter", "local", reporterDoc)
   125  }
   126  
   127  func usage() {
   128  	fmt.Fprintln(os.Stderr, usageMessage)
   129  	fmt.Fprintln(os.Stderr, "Flags:")
   130  	flag.PrintDefaults()
   131  	fmt.Fprintln(os.Stderr, "")
   132  	fmt.Fprintln(os.Stderr, "GitHub: https://github.com/haya14busa/reviewdog")
   133  	os.Exit(2)
   134  }
   135  
   136  func main() {
   137  	flag.Usage = usage
   138  	flag.Parse()
   139  	if err := run(os.Stdin, os.Stdout, opt); err != nil {
   140  		fmt.Fprintf(os.Stderr, "reviewdog: %v\n", err)
   141  		os.Exit(1)
   142  	}
   143  }
   144  
   145  func run(r io.Reader, w io.Writer, opt *option) error {
   146  	ctx := context.Background()
   147  
   148  	if opt.version {
   149  		fmt.Fprintln(w, reviewdog.Version)
   150  		return nil
   151  	}
   152  
   153  	if opt.list {
   154  		return runList(w)
   155  	}
   156  
   157  	// TODO(haya14busa): clean up when removing -ci flag from next release.
   158  	if opt.ci != "" {
   159  		return errors.New(`-ci flag is deprecated.
   160  See -reporter flag for migration and set -reporter="github-pr-review" or -reporter="github-pr-check" or -reporter="gitlab-mr-commit"`)
   161  	}
   162  
   163  	// assume it's project based run when both -efm ane -f are not specified
   164  	isProject := len(opt.efms) == 0 && opt.f == ""
   165  
   166  	var cs reviewdog.CommentService
   167  	var ds reviewdog.DiffService
   168  
   169  	if isProject {
   170  		cs = reviewdog.NewUnifiedCommentWriter(w)
   171  	} else {
   172  		cs = reviewdog.NewRawCommentWriter(w)
   173  	}
   174  
   175  	switch opt.reporter {
   176  	default:
   177  		return fmt.Errorf("unknown -reporter: %s", opt.reporter)
   178  	case "github-pr-check":
   179  		return runDoghouse(ctx, r, opt, isProject)
   180  	case "github-pr-review":
   181  		if os.Getenv("REVIEWDOG_GITHUB_API_TOKEN") == "" {
   182  			fmt.Fprintln(os.Stderr, "REVIEWDOG_GITHUB_API_TOKEN is not set")
   183  			return nil
   184  		}
   185  		gs, isPR, err := githubService(ctx)
   186  		if err != nil {
   187  			return err
   188  		}
   189  		if !isPR {
   190  			fmt.Fprintln(os.Stderr, "reviewdog: this is not PullRequest build.")
   191  			return nil
   192  		}
   193  		cs = reviewdog.MultiCommentService(gs, cs)
   194  		ds = gs
   195  	case "gitlab-mr-discussion":
   196  		build, cli, err := gitlabBuildWithClient()
   197  		if err != nil {
   198  			return err
   199  		}
   200  		if build.PullRequest == 0 {
   201  			fmt.Fprintln(os.Stderr, "this is not MergeRequest build.")
   202  			return nil
   203  		}
   204  
   205  		gc, err := reviewdog.NewGitLabMergeRequestDiscussionCommenter(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   206  		if err != nil {
   207  			return err
   208  		}
   209  
   210  		cs = reviewdog.MultiCommentService(gc, cs)
   211  		ds, err = reviewdog.NewGitLabMergeRequestDiff(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   212  		if err != nil {
   213  			return err
   214  		}
   215  	case "gitlab-mr-commit":
   216  		build, cli, err := gitlabBuildWithClient()
   217  		if err != nil {
   218  			return err
   219  		}
   220  		if build.PullRequest == 0 {
   221  			fmt.Fprintln(os.Stderr, "this is not MergeRequest build.")
   222  			return nil
   223  		}
   224  
   225  		gc, err := reviewdog.NewGitLabMergeRequestCommitCommenter(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   226  		if err != nil {
   227  			return err
   228  		}
   229  
   230  		cs = reviewdog.MultiCommentService(gc, cs)
   231  		ds, err = reviewdog.NewGitLabMergeRequestDiff(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   232  		if err != nil {
   233  			return err
   234  		}
   235  	case "local":
   236  		d, err := diffService(opt.diffCmd, opt.diffStrip)
   237  		if err != nil {
   238  			return err
   239  		}
   240  		ds = d
   241  	}
   242  
   243  	if isProject {
   244  		conf, err := projectConfig(opt.conf)
   245  		if err != nil {
   246  			return err
   247  		}
   248  		return project.Run(ctx, conf, cs, ds)
   249  	}
   250  
   251  	p, err := newParserFromOpt(opt)
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	app := reviewdog.NewReviewdog(toolName(opt), p, cs, ds)
   257  	return app.Run(ctx, r)
   258  }
   259  
   260  func runList(w io.Writer) error {
   261  	tabw := tabwriter.NewWriter(w, 0, 8, 0, '\t', 0)
   262  	for _, f := range sortedFmts(fmts.DefinedFmts()) {
   263  		fmt.Fprintf(tabw, "%s\t%s\t- %s\n", f.Name, f.Description, f.URL)
   264  	}
   265  	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "checkstyle", "checkstyle XML format", "http://checkstyle.sourceforge.net/")
   266  	return tabw.Flush()
   267  }
   268  
   269  type byFmtName []*fmts.Fmt
   270  
   271  func (p byFmtName) Len() int           { return len(p) }
   272  func (p byFmtName) Less(i, j int) bool { return p[i].Name < p[j].Name }
   273  func (p byFmtName) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
   274  
   275  func sortedFmts(fs fmts.Fmts) []*fmts.Fmt {
   276  	r := make([]*fmts.Fmt, 0, len(fs))
   277  	for _, f := range fs {
   278  		r = append(r, f)
   279  	}
   280  	sort.Sort(byFmtName(r))
   281  	return r
   282  }
   283  
   284  func diffService(s string, strip int) (reviewdog.DiffService, error) {
   285  	cmds, err := shellwords.Parse(s)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  	if len(cmds) < 1 {
   290  		return nil, errors.New("diff command is empty")
   291  	}
   292  	cmd := exec.Command(cmds[0], cmds[1:]...)
   293  	d := reviewdog.NewDiffCmd(cmd, strip)
   294  	return d, nil
   295  }
   296  
   297  func newHTTPClient() *http.Client {
   298  	tr := &http.Transport{
   299  		Proxy:           http.ProxyFromEnvironment,
   300  		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify()},
   301  	}
   302  	return &http.Client{Transport: tr}
   303  }
   304  
   305  func insecureSkipVerify() bool {
   306  	return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true"
   307  }
   308  
   309  func githubService(ctx context.Context) (githubservice *reviewdog.GitHubPullRequest, isPR bool, err error) {
   310  	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
   311  	if err != nil {
   312  		return nil, isPR, err
   313  	}
   314  	g, isPR, err := cienv.GetBuildInfo()
   315  	if err != nil {
   316  		return nil, isPR, err
   317  	}
   318  	// TODO: support commit build
   319  	if !isPR {
   320  		return nil, isPR, nil
   321  	}
   322  
   323  	client, err := githubClient(ctx, token)
   324  	if err != nil {
   325  		return nil, isPR, err
   326  	}
   327  
   328  	githubservice, err = reviewdog.NewGitHubPullReqest(client, g.Owner, g.Repo, g.PullRequest, g.SHA)
   329  	if err != nil {
   330  		return nil, isPR, err
   331  	}
   332  	return githubservice, isPR, nil
   333  }
   334  
   335  func githubClient(ctx context.Context, token string) (*github.Client, error) {
   336  	ctx = context.WithValue(ctx, oauth2.HTTPClient, newHTTPClient())
   337  	ts := oauth2.StaticTokenSource(
   338  		&oauth2.Token{AccessToken: token},
   339  	)
   340  	tc := oauth2.NewClient(ctx, ts)
   341  	client := github.NewClient(tc)
   342  	var err error
   343  	client.BaseURL, err = githubBaseURL()
   344  	return client, err
   345  }
   346  
   347  const defaultGitHubAPI = "https://api.github.com/"
   348  
   349  func githubBaseURL() (*url.URL, error) {
   350  	baseURL := os.Getenv("GITHUB_API")
   351  	if baseURL == "" {
   352  		baseURL = defaultGitHubAPI
   353  	}
   354  	u, err := url.Parse(baseURL)
   355  	if err != nil {
   356  		return nil, fmt.Errorf("GitHub base URL is invalid: %v, %v", baseURL, err)
   357  	}
   358  	return u, nil
   359  }
   360  
   361  func gitlabBuildWithClient() (*cienv.BuildInfo, *gitlab.Client, error) {
   362  	token, err := nonEmptyEnv("REVIEWDOG_GITLAB_API_TOKEN")
   363  	if err != nil {
   364  		return nil, nil, err
   365  	}
   366  
   367  	g, _, err := cienv.GetBuildInfo()
   368  	if err != nil {
   369  		return nil, nil, err
   370  	}
   371  
   372  	client, err := gitlabClient(token)
   373  	if err != nil {
   374  		return nil, nil, err
   375  	}
   376  
   377  	if g.PullRequest == 0 {
   378  		prNr, err := fetchMergeRequestIDFromCommit(client, g.Owner+"/"+g.Repo, g.SHA)
   379  		if err != nil {
   380  			return nil, nil, err
   381  		}
   382  		if prNr != 0 {
   383  			g.PullRequest = prNr
   384  		}
   385  	}
   386  
   387  	return g, client, err
   388  }
   389  
   390  func fetchMergeRequestIDFromCommit(cli *gitlab.Client, projectID string, sha string) (id int, err error) {
   391  	// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests
   392  	opt := &gitlab.ListProjectMergeRequestsOptions{
   393  		State:   gitlab.String("opened"),
   394  		OrderBy: gitlab.String("updated_at"),
   395  	}
   396  	mrs, _, err := cli.MergeRequests.ListProjectMergeRequests(projectID, opt)
   397  	if err != nil {
   398  		return 0, err
   399  	}
   400  	for _, mr := range mrs {
   401  		if mr.SHA == sha {
   402  			return mr.IID, nil
   403  		}
   404  	}
   405  	return 0, nil
   406  }
   407  
   408  func gitlabClient(token string) (*gitlab.Client, error) {
   409  	client := gitlab.NewClient(newHTTPClient(), token)
   410  	baseURL, err := gitlabBaseURL()
   411  	if err != nil {
   412  		return nil, err
   413  	}
   414  	if err := client.SetBaseURL(baseURL.String()); err != nil {
   415  		return nil, err
   416  	}
   417  	return client, nil
   418  }
   419  
   420  const defaultGitLabAPI = "https://gitlab.com/api/v4"
   421  
   422  func gitlabBaseURL() (*url.URL, error) {
   423  	baseURL := os.Getenv("GITLAB_API")
   424  	if baseURL == "" {
   425  		baseURL = defaultGitLabAPI
   426  	}
   427  	u, err := url.Parse(baseURL)
   428  	if err != nil {
   429  		return nil, fmt.Errorf("GitLab base URL is invalid: %v, %v", baseURL, err)
   430  	}
   431  	return u, nil
   432  }
   433  
   434  func nonEmptyEnv(env string) (string, error) {
   435  	v := os.Getenv(env)
   436  	if v == "" {
   437  		return "", fmt.Errorf("environment variable $%v is not set", env)
   438  	}
   439  	return v, nil
   440  }
   441  
   442  type strslice []string
   443  
   444  func (ss *strslice) String() string {
   445  	return fmt.Sprintf("%v", *ss)
   446  }
   447  
   448  func (ss *strslice) Set(value string) error {
   449  	*ss = append(*ss, value)
   450  	return nil
   451  }
   452  
   453  func projectConfig(path string) (*project.Config, error) {
   454  	b, err := readConf(path)
   455  	if err != nil {
   456  		return nil, fmt.Errorf("fail to open config: %v", err)
   457  	}
   458  	conf, err := project.Parse(b)
   459  	if err != nil {
   460  		return nil, fmt.Errorf("config is invalid: %v", err)
   461  	}
   462  	return conf, nil
   463  }
   464  
   465  func readConf(conf string) ([]byte, error) {
   466  	var conffiles []string
   467  	if conf != "" {
   468  		conffiles = []string{conf}
   469  	} else {
   470  		conffiles = []string{
   471  			".reviewdog.yaml",
   472  			".reviewdog.yml",
   473  			"reviewdog.yaml",
   474  			"reviewdog.yml",
   475  		}
   476  	}
   477  	for _, f := range conffiles {
   478  		bytes, err := ioutil.ReadFile(f)
   479  		if err == nil {
   480  			return bytes, nil
   481  		}
   482  	}
   483  	return nil, errors.New(".reviewdog.yml not found")
   484  }
   485  
   486  func newParserFromOpt(opt *option) (reviewdog.Parser, error) {
   487  	p, err := reviewdog.NewParser(&reviewdog.ParserOpt{FormatName: opt.f, Errorformat: opt.efms})
   488  	if err != nil {
   489  		return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %v", err)
   490  	}
   491  	return p, err
   492  }
   493  
   494  func toolName(opt *option) string {
   495  	name := opt.name
   496  	if name == "" && opt.f != "" {
   497  		name = opt.f
   498  	}
   499  	return name
   500  }