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 }