github.com/bgpat/reviewdog@v0.0.0-20230909064023-077e44ca1f66/cmd/reviewdog/main.go (about)

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