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