github.com/alexey-mercari/reviewdog@v0.10.1-0.20200514053941-928943b10766/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  	"strings"
    16  	"text/tabwriter"
    17  
    18  	"golang.org/x/build/gerrit"
    19  	"golang.org/x/net/context" // "context"
    20  	"golang.org/x/oauth2"
    21  
    22  	"github.com/google/go-github/v31/github"
    23  	"github.com/mattn/go-shellwords"
    24  	"github.com/reviewdog/errorformat/fmts"
    25  	"github.com/xanzy/go-gitlab"
    26  
    27  	"github.com/reviewdog/reviewdog"
    28  	"github.com/reviewdog/reviewdog/cienv"
    29  	"github.com/reviewdog/reviewdog/commands"
    30  	"github.com/reviewdog/reviewdog/difffilter"
    31  	"github.com/reviewdog/reviewdog/project"
    32  	gerritservice "github.com/reviewdog/reviewdog/service/gerrit"
    33  	githubservice "github.com/reviewdog/reviewdog/service/github"
    34  	"github.com/reviewdog/reviewdog/service/github/githubutils"
    35  	gitlabservice "github.com/reviewdog/reviewdog/service/gitlab"
    36  )
    37  
    38  const usageMessage = "" +
    39  	`Usage:	reviewdog [flags]
    40  	reviewdog accepts any compiler or linter results from stdin and filters
    41  	them by diff for review. reviewdog also can posts the results as a comment to
    42  	GitHub if you use reviewdog in CI service.`
    43  
    44  type option struct {
    45  	version          bool
    46  	diffCmd          string
    47  	diffStrip        int
    48  	efms             strslice
    49  	f                string // errorformat name
    50  	list             bool   // list supported errorformat name
    51  	name             string // tool name which is used in comment
    52  	ci               string
    53  	conf             string
    54  	runners          string
    55  	reporter         string
    56  	level            string
    57  	guessPullRequest bool
    58  	tee              bool
    59  	filterMode       difffilter.Mode
    60  	failOnError      bool
    61  }
    62  
    63  // flags doc
    64  const (
    65  	diffCmdDoc          = `diff command (e.g. "git diff") for local reporter. Do not use --relative flag for git command.`
    66  	diffStripDoc        = "strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)"
    67  	efmsDoc             = `list of errorformat (https://github.com/reviewdog/errorformat)`
    68  	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`
    69  	listDoc             = `list supported pre-defined format names which can be used as -f arg`
    70  	nameDoc             = `tool name in review comment. -f is used as tool name if -name is empty`
    71  	ciDoc               = `[deprecated] reviewdog automatically get necessary data. See also -reporter for migration`
    72  	confDoc             = `config file path`
    73  	runnersDoc          = `comma separated runners name to run in config file. default: run all runners`
    74  	levelDoc            = `report level currently used for github-pr-check reporter ("info","warning","error").`
    75  	guessPullRequestDoc = `guess Pull Request ID by branch name and commit SHA`
    76  	teeDoc              = `enable "tee"-like mode which outputs tools's output as is while reporting results to -reporter. Useful for debugging as well.`
    77  	filterModeDoc       = `how to filter checks results. [added, diff_context, file, nofilter].
    78  		"added" (default)
    79  			Filter by added/modified diff lines.
    80  		"diff_context"
    81  			Filter by diff context, which can include unchanged lines.
    82  			i.e. changed lines +-N lines (e.g. N=3 for default git diff).
    83  		"file"
    84  			Filter by added/modified file.
    85  		"nofilter"
    86  			Do not filter any results.
    87  `
    88  	reporterDoc = `reporter of reviewdog results. (local, github-check, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit)
    89  	"local" (default)
    90  		Report results to stdout.
    91  
    92  	"github-check"
    93  		Report results to GitHub Check. It works both for Pull Requests and commits.
    94  		For Pull Request, you can see report results in GitHub PullRequest Check
    95  		tab and can control filtering mode by -filter-mode flag.
    96  
    97  		There are two options to use this reporter.
    98  
    99  		Option 1) Run reviewdog from GitHub Actions w/ secrets.GITHUB_TOKEN
   100  			Note that it reports result to GitHub Actions log console for Pull
   101  			Requests from fork repository due to GitHub Actions restriction.
   102  			https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret
   103  
   104  			Set REVIEWDOG_GITHUB_API_TOKEN with secrets.GITHUB_TOKEN. e.g.
   105  					REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
   106  
   107  		Option 2) Install reviewdog GitHub Apps
   108  			1. Install reviewdog Apps. https://github.com/apps/reviewdog
   109  			2. Set REVIEWDOG_TOKEN or run reviewdog CLI in trusted CI providers.
   110  			You can get token from https://reviewdog.app/gh/<owner>/<repo-name>.
   111  			$ export REVIEWDOG_TOKEN="xxxxx"
   112  
   113  			Note: Token is not required if you run reviewdog in Travis CI.
   114  
   115  	"github-pr-check"
   116  		Same as github-check reporter but it only supports Pull Requests.
   117  
   118  	"github-pr-review"
   119  		Report results to GitHub review comments.
   120  
   121  		1. Set REVIEWDOG_GITHUB_API_TOKEN environment variable.
   122  		Go to https://github.com/settings/tokens and create new Personal access token with repo scope.
   123  
   124  		For GitHub Enterprise:
   125  			$ export GITHUB_API="https://example.githubenterprise.com/api/v3"
   126  
   127  	"gitlab-mr-discussion"
   128  		Report results to GitLab MergeRequest discussion.
   129  
   130  		1. Set REVIEWDOG_GITLAB_API_TOKEN environment variable.
   131  		Go to https://gitlab.com/profile/personal_access_tokens
   132  
   133  		CI_API_V4_URL (defined by Gitlab CI) as the base URL for the Gitlab API automatically.
   134  		Alternatively, GITLAB_API can also be defined, and it will take precedence over the former:
   135  			$ export GITLAB_API="https://example.gitlab.com/api/v4"
   136  
   137  	"gitlab-mr-commit"
   138  		Same as gitlab-mr-discussion, but report results to GitLab comments for
   139  		each commits in Merge Requests.
   140  
   141  	"gerrit-change-review"
   142  		Report results to Gerrit Change comments.
   143  
   144  		1. Set GERRIT_USERNAME and GERRIT_PASSWORD for basic authentication or
   145  		GIT_GITCOOKIE_PATH for git cookie based authentication.
   146  		2. Set GERRIT_CHANGE_ID, GERRIT_REVISION_ID GERRIT_BRANCH abd GERRIT_ADDRESS
   147  
   148  		For example:
   149  			$ export GERRIT_CHANGE_ID=myproject~master~I1293efab014de2
   150  			$ export GERRIT_REVISION_ID=ed318bf9a3c
   151  			$ export GERRIT_BRANCH=master
   152  			$ export GERRIT_ADDRESS=http://localhost:8080
   153  
   154  	For GitHub Enterprise and self hosted GitLab, set
   155  	REVIEWDOG_INSECURE_SKIP_VERIFY to skip verifying SSL (please use this at your own risk)
   156  		$ export REVIEWDOG_INSECURE_SKIP_VERIFY=true
   157  
   158  	For non-local reporters, reviewdog automatically get necessary data from
   159  	environment variable in CI service (GitHub Actions, Travis CI, Circle CI, drone.io, GitLab CI).
   160  	You can set necessary data with following environment variable manually if
   161  	you want (e.g. run reviewdog in Jenkins).
   162  
   163  		$ export CI_PULL_REQUEST=14 # Pull Request number (e.g. 14)
   164  		$ export CI_COMMIT="$(git rev-parse @)" # SHA1 for the current build
   165  		$ export CI_REPO_OWNER="haya14busa" # repository owner
   166  		$ export CI_REPO_NAME="reviewdog" # repository name
   167  `
   168  	failOnErrorDoc = `Returns 1 as exit code if any errors/warnings found in input`
   169  )
   170  
   171  var opt = &option{}
   172  
   173  func init() {
   174  	flag.BoolVar(&opt.version, "version", false, "print version")
   175  	flag.StringVar(&opt.diffCmd, "diff", "", diffCmdDoc)
   176  	flag.IntVar(&opt.diffStrip, "strip", 1, diffStripDoc)
   177  	flag.Var(&opt.efms, "efm", efmsDoc)
   178  	flag.StringVar(&opt.f, "f", "", fDoc)
   179  	flag.BoolVar(&opt.list, "list", false, listDoc)
   180  	flag.StringVar(&opt.name, "name", "", nameDoc)
   181  	flag.StringVar(&opt.ci, "ci", "", ciDoc)
   182  	flag.StringVar(&opt.conf, "conf", "", confDoc)
   183  	flag.StringVar(&opt.runners, "runners", "", runnersDoc)
   184  	flag.StringVar(&opt.reporter, "reporter", "local", reporterDoc)
   185  	flag.StringVar(&opt.level, "level", "error", levelDoc)
   186  	flag.BoolVar(&opt.guessPullRequest, "guess", false, guessPullRequestDoc)
   187  	flag.BoolVar(&opt.tee, "tee", false, teeDoc)
   188  	flag.Var(&opt.filterMode, "filter-mode", filterModeDoc)
   189  	flag.BoolVar(&opt.failOnError, "fail-on-error", false, failOnErrorDoc)
   190  }
   191  
   192  func usage() {
   193  	fmt.Fprintln(os.Stderr, usageMessage)
   194  	fmt.Fprintln(os.Stderr, "Flags:")
   195  	flag.PrintDefaults()
   196  	fmt.Fprintln(os.Stderr, "")
   197  	fmt.Fprintln(os.Stderr, "See https://github.com/reviewdog/reviewdog for more detail.")
   198  	os.Exit(2)
   199  }
   200  
   201  func main() {
   202  	flag.Usage = usage
   203  	flag.Parse()
   204  	if err := run(os.Stdin, os.Stdout, opt); err != nil {
   205  		fmt.Fprintf(os.Stderr, "reviewdog: %v\n", err)
   206  		os.Exit(1)
   207  	}
   208  }
   209  
   210  func run(r io.Reader, w io.Writer, opt *option) error {
   211  	ctx := context.Background()
   212  
   213  	if opt.version {
   214  		fmt.Fprintln(w, commands.Version)
   215  		return nil
   216  	}
   217  
   218  	if opt.list {
   219  		return runList(w)
   220  	}
   221  
   222  	// TODO(haya14busa): clean up when removing -ci flag from next release.
   223  	if opt.ci != "" {
   224  		return errors.New(`-ci flag is deprecated.
   225  See -reporter flag for migration and set -reporter="github-pr-review" or -reporter="github-pr-check" or -reporter="gitlab-mr-commit"`)
   226  	}
   227  
   228  	if opt.tee {
   229  		r = io.TeeReader(r, w)
   230  	}
   231  
   232  	// assume it's project based run when both -efm and -f are not specified
   233  	isProject := len(opt.efms) == 0 && opt.f == ""
   234  
   235  	var cs reviewdog.CommentService
   236  	var ds reviewdog.DiffService
   237  
   238  	if isProject {
   239  		cs = reviewdog.NewUnifiedCommentWriter(w)
   240  	} else {
   241  		cs = reviewdog.NewRawCommentWriter(w)
   242  	}
   243  
   244  	switch opt.reporter {
   245  	default:
   246  		return fmt.Errorf("unknown -reporter: %s", opt.reporter)
   247  	case "github-check":
   248  		return runDoghouse(ctx, r, w, opt, isProject, false)
   249  	case "github-pr-check":
   250  		return runDoghouse(ctx, r, w, opt, isProject, true)
   251  	case "github-pr-review":
   252  		if os.Getenv("REVIEWDOG_GITHUB_API_TOKEN") == "" {
   253  			fmt.Fprintln(os.Stderr, "REVIEWDOG_GITHUB_API_TOKEN is not set")
   254  			return nil
   255  		}
   256  		gs, isPR, err := githubService(ctx, opt)
   257  		if err != nil {
   258  			return err
   259  		}
   260  		if !isPR {
   261  			fmt.Fprintln(os.Stderr, "reviewdog: this is not PullRequest build.")
   262  			return nil
   263  		}
   264  		// If it's running in GitHub Actions and it's PR from forked repository,
   265  		// replace comment writer to GitHubActionLogWriter to create annotations
   266  		// instead of review comment because if it's PR from forked repository,
   267  		// GitHub token doesn't have write permission due to security concern and
   268  		// cannot post results via Review API.
   269  		if cienv.IsInGitHubAction() && isPRFromForkedRepo() {
   270  			fmt.Fprintln(w, `reviewdog: This is Pull-Request from forked repository.
   271  GitHub token doesn't have write permission of Review API, so reviewdog will
   272  report results via logging command [1] and create annotations similar to
   273  github-pr-check reporter as a fallback.
   274  
   275  [1]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands`)
   276  			cs = githubutils.NewGitHubActionLogWriter(opt.level)
   277  		} else {
   278  			cs = reviewdog.MultiCommentService(gs, cs)
   279  		}
   280  		ds = gs
   281  	case "gitlab-mr-discussion":
   282  		build, cli, err := gitlabBuildWithClient()
   283  		if err != nil {
   284  			return err
   285  		}
   286  		if build.PullRequest == 0 {
   287  			fmt.Fprintln(os.Stderr, "this is not MergeRequest build.")
   288  			return nil
   289  		}
   290  
   291  		gc, err := gitlabservice.NewGitLabMergeRequestDiscussionCommenter(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   292  		if err != nil {
   293  			return err
   294  		}
   295  
   296  		cs = reviewdog.MultiCommentService(gc, cs)
   297  		ds, err = gitlabservice.NewGitLabMergeRequestDiff(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   298  		if err != nil {
   299  			return err
   300  		}
   301  	case "gitlab-mr-commit":
   302  		build, cli, err := gitlabBuildWithClient()
   303  		if err != nil {
   304  			return err
   305  		}
   306  		if build.PullRequest == 0 {
   307  			fmt.Fprintln(os.Stderr, "this is not MergeRequest build.")
   308  			return nil
   309  		}
   310  
   311  		gc, err := gitlabservice.NewGitLabMergeRequestCommitCommenter(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   312  		if err != nil {
   313  			return err
   314  		}
   315  
   316  		cs = reviewdog.MultiCommentService(gc, cs)
   317  		ds, err = gitlabservice.NewGitLabMergeRequestDiff(cli, build.Owner, build.Repo, build.PullRequest, build.SHA)
   318  		if err != nil {
   319  			return err
   320  		}
   321  	case "gerrit-change-review":
   322  		b, cli, err := gerritBuildWithClient()
   323  		if err != nil {
   324  			return err
   325  		}
   326  		gc, err := gerritservice.NewChangeReviewCommenter(cli, b.GerritChangeID, b.GerritRevisionID)
   327  		if err != nil {
   328  			return err
   329  		}
   330  		cs = gc
   331  
   332  		d, err := gerritservice.NewChangeDiff(cli, b.Branch, b.GerritChangeID)
   333  		if err != nil {
   334  			return err
   335  		}
   336  		ds = d
   337  	case "local":
   338  		if opt.diffCmd == "" && opt.filterMode == difffilter.ModeNoFilter {
   339  			ds = &reviewdog.EmptyDiff{}
   340  		} else {
   341  			d, err := diffService(opt.diffCmd, opt.diffStrip)
   342  			if err != nil {
   343  				return err
   344  			}
   345  			ds = d
   346  		}
   347  	}
   348  
   349  	if isProject {
   350  		conf, err := projectConfig(opt.conf)
   351  		if err != nil {
   352  			return err
   353  		}
   354  		return project.Run(ctx, conf, buildRunnersMap(opt.runners), cs, ds, opt.tee, opt.filterMode, opt.failOnError)
   355  	}
   356  
   357  	p, err := newParserFromOpt(opt)
   358  	if err != nil {
   359  		return err
   360  	}
   361  
   362  	app := reviewdog.NewReviewdog(toolName(opt), p, cs, ds, opt.filterMode, opt.failOnError)
   363  	return app.Run(ctx, r)
   364  }
   365  
   366  func runList(w io.Writer) error {
   367  	tabw := tabwriter.NewWriter(w, 0, 8, 0, '\t', 0)
   368  	for _, f := range sortedFmts(fmts.DefinedFmts()) {
   369  		fmt.Fprintf(tabw, "%s\t%s\t- %s\n", f.Name, f.Description, f.URL)
   370  	}
   371  	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "checkstyle", "checkstyle XML format", "http://checkstyle.sourceforge.net/")
   372  	return tabw.Flush()
   373  }
   374  
   375  type byFmtName []*fmts.Fmt
   376  
   377  func (p byFmtName) Len() int           { return len(p) }
   378  func (p byFmtName) Less(i, j int) bool { return p[i].Name < p[j].Name }
   379  func (p byFmtName) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
   380  
   381  func sortedFmts(fs fmts.Fmts) []*fmts.Fmt {
   382  	r := make([]*fmts.Fmt, 0, len(fs))
   383  	for _, f := range fs {
   384  		r = append(r, f)
   385  	}
   386  	sort.Sort(byFmtName(r))
   387  	return r
   388  }
   389  
   390  func diffService(s string, strip int) (reviewdog.DiffService, error) {
   391  	cmds, err := shellwords.Parse(s)
   392  	if err != nil {
   393  		return nil, err
   394  	}
   395  	if len(cmds) < 1 {
   396  		return nil, errors.New("diff command is empty")
   397  	}
   398  	cmd := exec.Command(cmds[0], cmds[1:]...)
   399  	d := reviewdog.NewDiffCmd(cmd, strip)
   400  	return d, nil
   401  }
   402  
   403  func newHTTPClient() *http.Client {
   404  	tr := &http.Transport{
   405  		Proxy:           http.ProxyFromEnvironment,
   406  		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify()},
   407  	}
   408  	return &http.Client{Transport: tr}
   409  }
   410  
   411  func insecureSkipVerify() bool {
   412  	return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true"
   413  }
   414  
   415  func githubService(ctx context.Context, opt *option) (gs *githubservice.GitHubPullRequest, isPR bool, err error) {
   416  	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
   417  	if err != nil {
   418  		return nil, isPR, err
   419  	}
   420  	g, isPR, err := cienv.GetBuildInfo()
   421  	if err != nil {
   422  		return nil, isPR, err
   423  	}
   424  
   425  	client, err := githubClient(ctx, token)
   426  	if err != nil {
   427  		return nil, isPR, err
   428  	}
   429  
   430  	if !isPR {
   431  		if !opt.guessPullRequest {
   432  			return nil, false, nil
   433  		}
   434  
   435  		if g.Branch == "" && g.SHA == "" {
   436  			return nil, false, nil
   437  		}
   438  
   439  		prID, err := getPullRequestIDByBranchOrCommit(ctx, client, g)
   440  		if err != nil {
   441  			fmt.Fprintln(os.Stderr, err)
   442  			return nil, false, nil
   443  		}
   444  		g.PullRequest = prID
   445  	}
   446  
   447  	gs, err = githubservice.NewGitHubPullRequest(client, g.Owner, g.Repo, g.PullRequest, g.SHA)
   448  	if err != nil {
   449  		return nil, false, err
   450  	}
   451  	return gs, true, nil
   452  }
   453  
   454  func getPullRequestIDByBranchOrCommit(ctx context.Context, client *github.Client, info *cienv.BuildInfo) (int, error) {
   455  	options := &github.SearchOptions{
   456  		Sort:  "updated",
   457  		Order: "desc",
   458  	}
   459  
   460  	query := []string{
   461  		"type:pr",
   462  		"state:open",
   463  		fmt.Sprintf("repo:%s/%s", info.Owner, info.Repo),
   464  	}
   465  	if info.Branch != "" {
   466  		query = append(query, fmt.Sprintf("head:%s", info.Branch))
   467  	}
   468  	if info.SHA != "" {
   469  		query = append(query, info.SHA)
   470  	}
   471  
   472  	preparedQuery := strings.Join(query, " ")
   473  	pullRequests, _, err := client.Search.Issues(ctx, preparedQuery, options)
   474  	if err != nil {
   475  		return 0, err
   476  	}
   477  
   478  	if *pullRequests.Total == 0 {
   479  		return 0, fmt.Errorf("reviewdog: PullRequest not found, query: %s", preparedQuery)
   480  	}
   481  
   482  	return *pullRequests.Issues[0].Number, nil
   483  }
   484  
   485  func githubClient(ctx context.Context, token string) (*github.Client, error) {
   486  	ctx = context.WithValue(ctx, oauth2.HTTPClient, newHTTPClient())
   487  	ts := oauth2.StaticTokenSource(
   488  		&oauth2.Token{AccessToken: token},
   489  	)
   490  	tc := oauth2.NewClient(ctx, ts)
   491  	client := github.NewClient(tc)
   492  	var err error
   493  	client.BaseURL, err = githubBaseURL()
   494  	return client, err
   495  }
   496  
   497  const defaultGitHubAPI = "https://api.github.com/"
   498  
   499  func githubBaseURL() (*url.URL, error) {
   500  	baseURL := os.Getenv("GITHUB_API")
   501  	if baseURL == "" {
   502  		baseURL = defaultGitHubAPI
   503  	}
   504  	u, err := url.Parse(baseURL)
   505  	if err != nil {
   506  		return nil, fmt.Errorf("GitHub base URL is invalid: %v, %v", baseURL, err)
   507  	}
   508  	return u, nil
   509  }
   510  
   511  func gitlabBuildWithClient() (*cienv.BuildInfo, *gitlab.Client, error) {
   512  	token, err := nonEmptyEnv("REVIEWDOG_GITLAB_API_TOKEN")
   513  	if err != nil {
   514  		return nil, nil, err
   515  	}
   516  
   517  	g, _, err := cienv.GetBuildInfo()
   518  	if err != nil {
   519  		return nil, nil, err
   520  	}
   521  
   522  	client, err := gitlabClient(token)
   523  	if err != nil {
   524  		return nil, nil, err
   525  	}
   526  
   527  	if g.PullRequest == 0 {
   528  		prNr, err := fetchMergeRequestIDFromCommit(client, g.Owner+"/"+g.Repo, g.SHA)
   529  		if err != nil {
   530  			return nil, nil, err
   531  		}
   532  		if prNr != 0 {
   533  			g.PullRequest = prNr
   534  		}
   535  	}
   536  
   537  	return g, client, err
   538  }
   539  
   540  func gerritBuildWithClient() (*cienv.BuildInfo, *gerrit.Client, error) {
   541  	buildInfo, err := cienv.GetGerritBuildInfo()
   542  	if err != nil {
   543  		return nil, nil, err
   544  	}
   545  
   546  	gerritAddr := os.Getenv("GERRIT_ADDRESS")
   547  	if gerritAddr == "" {
   548  		return nil, nil, errors.New("cannot get gerrit host address from environment variable. Set GERRIT_ADDRESS ?")
   549  	}
   550  
   551  	username := os.Getenv("GERRIT_USERNAME")
   552  	password := os.Getenv("GERRIT_PASSWORD")
   553  	if username != "" && password != "" {
   554  		client := gerrit.NewClient(gerritAddr, gerrit.BasicAuth(username, password))
   555  		return buildInfo, client, nil
   556  	}
   557  
   558  	if useGitCookiePath := os.Getenv("GERRIT_GIT_COOKIE_PATH"); useGitCookiePath != "" {
   559  		client := gerrit.NewClient(gerritAddr, gerrit.GitCookieFileAuth(useGitCookiePath))
   560  		return buildInfo, client, nil
   561  	}
   562  
   563  	client := gerrit.NewClient(gerritAddr, gerrit.NoAuth)
   564  	return buildInfo, client, nil
   565  }
   566  
   567  func fetchMergeRequestIDFromCommit(cli *gitlab.Client, projectID, sha string) (id int, err error) {
   568  	// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests
   569  	opt := &gitlab.ListProjectMergeRequestsOptions{
   570  		State:   gitlab.String("opened"),
   571  		OrderBy: gitlab.String("updated_at"),
   572  	}
   573  	mrs, _, err := cli.MergeRequests.ListProjectMergeRequests(projectID, opt)
   574  	if err != nil {
   575  		return 0, err
   576  	}
   577  	for _, mr := range mrs {
   578  		if mr.SHA == sha {
   579  			return mr.IID, nil
   580  		}
   581  	}
   582  	return 0, nil
   583  }
   584  
   585  func gitlabClient(token string) (*gitlab.Client, error) {
   586  	baseURL, err := gitlabBaseURL()
   587  	if err != nil {
   588  		return nil, err
   589  	}
   590  	client, err := gitlab.NewClient(token, gitlab.WithHTTPClient(newHTTPClient()), gitlab.WithBaseURL(baseURL.String()))
   591  	if err != nil {
   592  		return nil, err
   593  	}
   594  	return client, nil
   595  }
   596  
   597  const defaultGitLabAPI = "https://gitlab.com/api/v4"
   598  
   599  func gitlabBaseURL() (*url.URL, error) {
   600  	gitlabAPI := os.Getenv("GITLAB_API")
   601  	gitlabV4URL := os.Getenv("CI_API_V4_URL")
   602  
   603  	var baseURL string
   604  	if gitlabAPI != "" {
   605  		baseURL = gitlabAPI
   606  	} else if gitlabV4URL != "" {
   607  		baseURL = gitlabV4URL
   608  	} else {
   609  		baseURL = defaultGitLabAPI
   610  	}
   611  
   612  	u, err := url.Parse(baseURL)
   613  	if err != nil {
   614  		return nil, fmt.Errorf("GitLab base URL is invalid: %v, %v", baseURL, err)
   615  	}
   616  	return u, nil
   617  }
   618  
   619  func nonEmptyEnv(env string) (string, error) {
   620  	v := os.Getenv(env)
   621  	if v == "" {
   622  		return "", fmt.Errorf("environment variable $%v is not set", env)
   623  	}
   624  	return v, nil
   625  }
   626  
   627  type strslice []string
   628  
   629  func (ss *strslice) String() string {
   630  	return fmt.Sprintf("%v", *ss)
   631  }
   632  
   633  func (ss *strslice) Set(value string) error {
   634  	*ss = append(*ss, value)
   635  	return nil
   636  }
   637  
   638  func projectConfig(path string) (*project.Config, error) {
   639  	b, err := readConf(path)
   640  	if err != nil {
   641  		return nil, fmt.Errorf("fail to open config: %v", err)
   642  	}
   643  	conf, err := project.Parse(b)
   644  	if err != nil {
   645  		return nil, fmt.Errorf("config is invalid: %v", err)
   646  	}
   647  	return conf, nil
   648  }
   649  
   650  func readConf(conf string) ([]byte, error) {
   651  	var conffiles []string
   652  	if conf != "" {
   653  		conffiles = []string{conf}
   654  	} else {
   655  		conffiles = []string{
   656  			".reviewdog.yaml",
   657  			".reviewdog.yml",
   658  			"reviewdog.yaml",
   659  			"reviewdog.yml",
   660  		}
   661  	}
   662  	for _, f := range conffiles {
   663  		bytes, err := ioutil.ReadFile(f)
   664  		if err == nil {
   665  			return bytes, nil
   666  		}
   667  	}
   668  	return nil, errors.New(".reviewdog.yml not found")
   669  }
   670  
   671  func newParserFromOpt(opt *option) (reviewdog.Parser, error) {
   672  	p, err := reviewdog.NewParser(&reviewdog.ParserOpt{FormatName: opt.f, Errorformat: opt.efms})
   673  	if err != nil {
   674  		return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %v", err)
   675  	}
   676  	return p, err
   677  }
   678  
   679  func toolName(opt *option) string {
   680  	name := opt.name
   681  	if name == "" && opt.f != "" {
   682  		name = opt.f
   683  	}
   684  	return name
   685  }
   686  
   687  func buildRunnersMap(runners string) map[string]bool {
   688  	m := make(map[string]bool)
   689  	for _, r := range strings.Split(runners, ",") {
   690  		if name := strings.TrimSpace(r); name != "" {
   691  			m[name] = true
   692  		}
   693  	}
   694  	return m
   695  }