github.com/Mistwind/reviewdog@v0.0.0-20230317041057-48e69b6d9e86/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/reviewdog/reviewdog"
    28  	"github.com/reviewdog/reviewdog/cienv"
    29  	"github.com/reviewdog/reviewdog/commands"
    30  	"github.com/reviewdog/reviewdog/filter"
    31  	"github.com/reviewdog/reviewdog/parser"
    32  	"github.com/reviewdog/reviewdog/project"
    33  	bbservice "github.com/reviewdog/reviewdog/service/bitbucket"
    34  	gerritservice "github.com/reviewdog/reviewdog/service/gerrit"
    35  	githubservice "github.com/reviewdog/reviewdog/service/github"
    36  	"github.com/reviewdog/reviewdog/service/github/githubutils"
    37  	gitlabservice "github.com/reviewdog/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/reviewdog/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  		gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA)
   349  		if err != nil {
   350  			return err
   351  		}
   352  		log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n")
   353  
   354  		cs = reviewdog.MultiCommentService(gc, cs)
   355  		ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA)
   356  		if err != nil {
   357  			return err
   358  		}
   359  		log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n")
   360  
   361  	case "gerrit-change-review":
   362  		b, cli, err := gerritBuildWithClient()
   363  		if err != nil {
   364  			return err
   365  		}
   366  		gc, err := gerritservice.NewChangeReviewCommenter(cli, b.GerritChangeID, b.GerritRevisionID)
   367  		if err != nil {
   368  			return err
   369  		}
   370  		cs = gc
   371  
   372  		d, err := gerritservice.NewChangeDiff(cli, b.Branch, b.GerritChangeID)
   373  		if err != nil {
   374  			return err
   375  		}
   376  		ds = d
   377  	case "bitbucket-code-report":
   378  		build, client, ct, err := bitbucketBuildWithClient(ctx)
   379  		if err != nil {
   380  			return err
   381  		}
   382  		ctx = ct
   383  
   384  		cs = bbservice.NewReportAnnotator(client,
   385  			build.Owner, build.Repo, build.SHA, getRunnersList(opt, projectConf))
   386  
   387  		if !(opt.filterMode == filter.ModeDefault || opt.filterMode == filter.ModeNoFilter) {
   388  			// by default scan whole project with out diff (filter.ModeNoFilter)
   389  			// Bitbucket pipelines doesn't give an easy way to know
   390  			// which commit run pipeline before so we can compare between them
   391  			// however once PR is opened, Bitbucket Reports UI will do automatic
   392  			// filtering of annotations dividing them in two groups:
   393  			// - This pull request (10)
   394  			// - All (50)
   395  			log.Printf("reviewdog: [bitbucket-code-report] supports only with filter.ModeNoFilter for now")
   396  		}
   397  		opt.filterMode = filter.ModeNoFilter
   398  		ds = &reviewdog.EmptyDiff{}
   399  	case "local":
   400  		if opt.diffCmd == "" && opt.filterMode == filter.ModeNoFilter {
   401  			ds = &reviewdog.EmptyDiff{}
   402  		} else {
   403  			d, err := diffService(opt.diffCmd, opt.diffStrip)
   404  			if err != nil {
   405  				return err
   406  			}
   407  			ds = d
   408  		}
   409  	}
   410  
   411  	if isProject {
   412  		return project.Run(ctx, projectConf, buildRunnersMap(opt.runners), cs, ds, opt.tee, opt.filterMode, opt.failOnError)
   413  	}
   414  
   415  	p, err := newParserFromOpt(opt)
   416  	if err != nil {
   417  		return err
   418  	}
   419  	log.Printf("reviewdog: newParserFromOpt ok\n")
   420  
   421  	app := reviewdog.NewReviewdog(toolName(opt), p, cs, ds, opt.filterMode, opt.failOnError)
   422  	return app.Run(ctx, r)
   423  }
   424  
   425  func runList(w io.Writer) error {
   426  	tabw := tabwriter.NewWriter(w, 0, 8, 0, '\t', 0)
   427  	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjson", "Reviewdog Diagnostic JSON Format (JSON of DiagnosticResult message)", "https://github.com/reviewdog/reviewdog")
   428  	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjsonl", "Reviewdog Diagnostic JSONL Format (JSONL of Diagnostic message)", "https://github.com/reviewdog/reviewdog")
   429  	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "diff", "Unified Diff Format", "https://en.wikipedia.org/wiki/Diff#Unified_format")
   430  	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "checkstyle", "checkstyle XML format", "http://checkstyle.sourceforge.net/")
   431  	for _, f := range sortedFmts(fmts.DefinedFmts()) {
   432  		fmt.Fprintf(tabw, "%s\t%s\t- %s\n", f.Name, f.Description, f.URL)
   433  	}
   434  	return tabw.Flush()
   435  }
   436  
   437  type byFmtName []*fmts.Fmt
   438  
   439  func (p byFmtName) Len() int           { return len(p) }
   440  func (p byFmtName) Less(i, j int) bool { return p[i].Name < p[j].Name }
   441  func (p byFmtName) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }
   442  
   443  func sortedFmts(fs fmts.Fmts) []*fmts.Fmt {
   444  	r := make([]*fmts.Fmt, 0, len(fs))
   445  	for _, f := range fs {
   446  		r = append(r, f)
   447  	}
   448  	sort.Sort(byFmtName(r))
   449  	return r
   450  }
   451  
   452  func diffService(s string, strip int) (reviewdog.DiffService, error) {
   453  	cmds, err := shellwords.Parse(s)
   454  	if err != nil {
   455  		return nil, err
   456  	}
   457  	if len(cmds) < 1 {
   458  		return nil, errors.New("diff command is empty")
   459  	}
   460  	cmd := exec.Command(cmds[0], cmds[1:]...)
   461  	d := reviewdog.NewDiffCmd(cmd, strip)
   462  	return d, nil
   463  }
   464  
   465  func newHTTPClient() *http.Client {
   466  	tr := &http.Transport{
   467  		Proxy:           http.ProxyFromEnvironment,
   468  		TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify()},
   469  	}
   470  	return &http.Client{Transport: tr}
   471  }
   472  
   473  func insecureSkipVerify() bool {
   474  	return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true"
   475  }
   476  
   477  func githubService(ctx context.Context, opt *option) (gs *githubservice.PullRequest, isPR bool, err error) {
   478  	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
   479  	if err != nil {
   480  		return nil, isPR, err
   481  	}
   482  	g, isPR, err := cienv.GetBuildInfo()
   483  	if err != nil {
   484  		return nil, isPR, err
   485  	}
   486  
   487  	client, err := githubClient(ctx, token)
   488  	if err != nil {
   489  		return nil, isPR, err
   490  	}
   491  
   492  	if !isPR {
   493  		if !opt.guessPullRequest {
   494  			return nil, false, nil
   495  		}
   496  
   497  		if g.Branch == "" && g.SHA == "" {
   498  			return nil, false, nil
   499  		}
   500  
   501  		prID, err := getPullRequestIDByBranchOrCommit(ctx, client, g)
   502  		if err != nil {
   503  			fmt.Fprintln(os.Stderr, err)
   504  			return nil, false, nil
   505  		}
   506  		g.PullRequest = prID
   507  	}
   508  
   509  	gs, err = githubservice.NewGitHubPullRequest(client, g.Owner, g.Repo, g.PullRequest, g.SHA)
   510  	if err != nil {
   511  		return nil, false, err
   512  	}
   513  	return gs, true, nil
   514  }
   515  
   516  func getPullRequestIDByBranchOrCommit(ctx context.Context, client *github.Client, info *cienv.BuildInfo) (int, error) {
   517  	options := &github.SearchOptions{
   518  		Sort:  "updated",
   519  		Order: "desc",
   520  	}
   521  
   522  	query := []string{
   523  		"type:pr",
   524  		"state:open",
   525  		fmt.Sprintf("repo:%s/%s", info.Owner, info.Repo),
   526  	}
   527  	if info.Branch != "" {
   528  		query = append(query, fmt.Sprintf("head:%s", info.Branch))
   529  	}
   530  	if info.SHA != "" {
   531  		query = append(query, info.SHA)
   532  	}
   533  
   534  	preparedQuery := strings.Join(query, " ")
   535  	pullRequests, _, err := client.Search.Issues(ctx, preparedQuery, options)
   536  	if err != nil {
   537  		return 0, err
   538  	}
   539  
   540  	if *pullRequests.Total == 0 {
   541  		return 0, fmt.Errorf("reviewdog: PullRequest not found, query: %s", preparedQuery)
   542  	}
   543  
   544  	return *pullRequests.Issues[0].Number, nil
   545  }
   546  
   547  func githubClient(ctx context.Context, token string) (*github.Client, error) {
   548  	ctx = context.WithValue(ctx, oauth2.HTTPClient, newHTTPClient())
   549  	ts := oauth2.StaticTokenSource(
   550  		&oauth2.Token{AccessToken: token},
   551  	)
   552  	tc := oauth2.NewClient(ctx, ts)
   553  	client := github.NewClient(tc)
   554  	var err error
   555  	client.BaseURL, err = githubBaseURL()
   556  	return client, err
   557  }
   558  
   559  const defaultGitHubAPI = "https://api.github.com/"
   560  
   561  func githubBaseURL() (*url.URL, error) {
   562  	if baseURL := os.Getenv("GITHUB_API"); baseURL != "" {
   563  		u, err := url.Parse(baseURL)
   564  		if err != nil {
   565  			return nil, fmt.Errorf("GitHub base URL from GITHUB_API is invalid: %v, %w", baseURL, err)
   566  		}
   567  		return u, nil
   568  	}
   569  	// get GitHub base URL from GitHub Actions' default environment variable GITHUB_API_URL
   570  	// ref: https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables
   571  	if baseURL := os.Getenv("GITHUB_API_URL"); baseURL != "" {
   572  		u, err := url.Parse(baseURL + "/")
   573  		if err != nil {
   574  			return nil, fmt.Errorf("GitHub base URL from GITHUB_API_URL is invalid: %v, %w", baseURL, err)
   575  		}
   576  		return u, nil
   577  	}
   578  	u, err := url.Parse(defaultGitHubAPI)
   579  	if err != nil {
   580  		return nil, fmt.Errorf("GitHub base URL from reviewdog default is invalid: %v, %w", defaultGitHubAPI, err)
   581  	}
   582  	return u, nil
   583  }
   584  
   585  func gitlabBuildWithClient(reporter string) (*cienv.BuildInfo, *gitlab.Client, error) {
   586  	token, err := nonEmptyEnv("REVIEWDOG_GITLAB_API_TOKEN")
   587  	if err != nil {
   588  		return nil, nil, err
   589  	}
   590  
   591  	g, _, err := cienv.GetBuildInfo()
   592  	if err != nil {
   593  		return nil, nil, err
   594  	}
   595  
   596  	client, err := gitlabClient(token)
   597  	if err != nil {
   598  		return nil, nil, err
   599  	}
   600  
   601  	if reporter == "gitlab-mr-commit" || reporter == "gitlab-mr-discussion" {
   602  		if g.PullRequest == 0 {
   603  			prNr, err := fetchMergeRequestIDFromCommit(client, g.Owner+"/"+g.Repo, g.SHA)
   604  			if err != nil {
   605  				return nil, nil, err
   606  			}
   607  			if prNr != 0 {
   608  				g.PullRequest = prNr
   609  			}
   610  		}
   611  	}
   612  
   613  	return g, client, err
   614  }
   615  
   616  func gerritBuildWithClient() (*cienv.BuildInfo, *gerrit.Client, error) {
   617  	buildInfo, err := cienv.GetGerritBuildInfo()
   618  	if err != nil {
   619  		return nil, nil, err
   620  	}
   621  
   622  	gerritAddr := os.Getenv("GERRIT_ADDRESS")
   623  	if gerritAddr == "" {
   624  		return nil, nil, errors.New("cannot get gerrit host address from environment variable. Set GERRIT_ADDRESS ?")
   625  	}
   626  
   627  	username := os.Getenv("GERRIT_USERNAME")
   628  	password := os.Getenv("GERRIT_PASSWORD")
   629  	if username != "" && password != "" {
   630  		client := gerrit.NewClient(gerritAddr, gerrit.BasicAuth(username, password))
   631  		return buildInfo, client, nil
   632  	}
   633  
   634  	if useGitCookiePath := os.Getenv("GERRIT_GIT_COOKIE_PATH"); useGitCookiePath != "" {
   635  		client := gerrit.NewClient(gerritAddr, gerrit.GitCookieFileAuth(useGitCookiePath))
   636  		return buildInfo, client, nil
   637  	}
   638  
   639  	client := gerrit.NewClient(gerritAddr, gerrit.NoAuth)
   640  	return buildInfo, client, nil
   641  }
   642  
   643  func bitbucketBuildWithClient(ctx context.Context) (*cienv.BuildInfo, bbservice.APIClient, context.Context, error) {
   644  	build, _, err := cienv.GetBuildInfo()
   645  	if err != nil {
   646  		return nil, nil, ctx, err
   647  	}
   648  
   649  	bbUser := os.Getenv("BITBUCKET_USER")
   650  	bbPass := os.Getenv("BITBUCKET_PASSWORD")
   651  	bbAccessToken := os.Getenv("BITBUCKET_ACCESS_TOKEN")
   652  	bbServerURL := os.Getenv("BITBUCKET_SERVER_URL")
   653  
   654  	var client bbservice.APIClient
   655  	if bbServerURL != "" {
   656  		ctx, err = bbservice.BuildServerAPIContext(ctx, bbServerURL, bbUser, bbPass, bbAccessToken)
   657  		if err != nil {
   658  			return nil, nil, ctx, fmt.Errorf("failed to build context for Bitbucket API calls: %w", err)
   659  		}
   660  		client = bbservice.NewServerAPIClient()
   661  	} else {
   662  		ctx = bbservice.BuildCloudAPIContext(ctx, bbUser, bbPass, bbAccessToken)
   663  		client = bbservice.NewCloudAPIClient(cienv.IsInBitbucketPipeline(), cienv.IsInBitbucketPipe())
   664  	}
   665  
   666  	return build, client, ctx, nil
   667  }
   668  
   669  func fetchMergeRequestIDFromCommit(cli *gitlab.Client, projectID, sha string) (id int, err error) {
   670  	// https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests
   671  	opt := &gitlab.ListProjectMergeRequestsOptions{
   672  		State:   gitlab.String("opened"),
   673  		OrderBy: gitlab.String("updated_at"),
   674  	}
   675  	mrs, _, err := cli.MergeRequests.ListProjectMergeRequests(projectID, opt)
   676  	if err != nil {
   677  		return 0, err
   678  	}
   679  	for _, mr := range mrs {
   680  		if mr.SHA == sha {
   681  			return mr.IID, nil
   682  		}
   683  	}
   684  	return 0, nil
   685  }
   686  
   687  func gitlabClient(token string) (*gitlab.Client, error) {
   688  	baseURL, err := gitlabBaseURL()
   689  	if err != nil {
   690  		return nil, err
   691  	}
   692  	client, err := gitlab.NewClient(token, gitlab.WithHTTPClient(newHTTPClient()), gitlab.WithBaseURL(baseURL.String()))
   693  	if err != nil {
   694  		return nil, err
   695  	}
   696  	return client, nil
   697  }
   698  
   699  const defaultGitLabAPI = "https://gitlab.com/api/v4"
   700  
   701  func gitlabBaseURL() (*url.URL, error) {
   702  	gitlabAPI := os.Getenv("GITLAB_API")
   703  	gitlabV4URL := os.Getenv("CI_API_V4_URL")
   704  
   705  	var baseURL string
   706  	if gitlabAPI != "" {
   707  		baseURL = gitlabAPI
   708  	} else if gitlabV4URL != "" {
   709  		baseURL = gitlabV4URL
   710  	} else {
   711  		baseURL = defaultGitLabAPI
   712  	}
   713  
   714  	u, err := url.Parse(baseURL)
   715  	if err != nil {
   716  		return nil, fmt.Errorf("GitLab base URL is invalid: %v, %w", baseURL, err)
   717  	}
   718  	return u, nil
   719  }
   720  
   721  func nonEmptyEnv(env string) (string, error) {
   722  	v := os.Getenv(env)
   723  	if v == "" {
   724  		return "", fmt.Errorf("environment variable $%v is not set", env)
   725  	}
   726  	return v, nil
   727  }
   728  
   729  type strslice []string
   730  
   731  func (ss *strslice) String() string {
   732  	return fmt.Sprintf("%v", *ss)
   733  }
   734  
   735  func (ss *strslice) Set(value string) error {
   736  	*ss = append(*ss, value)
   737  	return nil
   738  }
   739  
   740  func projectConfig(path string) (*project.Config, error) {
   741  	b, err := readConf(path)
   742  	if err != nil {
   743  		return nil, fmt.Errorf("fail to open config: %w", err)
   744  	}
   745  	conf, err := project.Parse(b)
   746  	if err != nil {
   747  		return nil, fmt.Errorf("config is invalid: %w", err)
   748  	}
   749  	return conf, nil
   750  }
   751  
   752  func readConf(conf string) ([]byte, error) {
   753  	var conffiles []string
   754  	if conf != "" {
   755  		conffiles = []string{conf}
   756  	} else {
   757  		conffiles = []string{
   758  			".reviewdog.yaml",
   759  			".reviewdog.yml",
   760  			"reviewdog.yaml",
   761  			"reviewdog.yml",
   762  		}
   763  	}
   764  	for _, f := range conffiles {
   765  		bytes, err := os.ReadFile(f)
   766  		if err == nil {
   767  			return bytes, nil
   768  		}
   769  	}
   770  	return nil, errors.New(".reviewdog.yml not found")
   771  }
   772  
   773  func newParserFromOpt(opt *option) (parser.Parser, error) {
   774  	p, err := parser.New(&parser.Option{
   775  		FormatName:  opt.f,
   776  		DiffStrip:   opt.fDiffStrip,
   777  		Errorformat: opt.efms,
   778  	})
   779  	if err != nil {
   780  		return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %w", err)
   781  	}
   782  	return p, err
   783  }
   784  
   785  func toolName(opt *option) string {
   786  	name := opt.name
   787  	if name == "" && opt.f != "" {
   788  		name = opt.f
   789  	}
   790  	return name
   791  }
   792  
   793  func buildRunnersMap(runners string) map[string]bool {
   794  	m := make(map[string]bool)
   795  	for _, r := range strings.Split(runners, ",") {
   796  		if name := strings.TrimSpace(r); name != "" {
   797  			m[name] = true
   798  		}
   799  	}
   800  	return m
   801  }
   802  
   803  func getRunnersList(opt *option, conf *project.Config) []string {
   804  	if len(opt.runners) > 0 { // if runners explicitly defined, use them
   805  		return strings.Split(opt.runners, ",")
   806  	}
   807  
   808  	if conf != nil { // if this is a Project run, and no explicitly provided runners
   809  		// if no runners explicitly provided
   810  		// get all runners from config
   811  		list := make([]string, 0, len(conf.Runner))
   812  		for runner := range conf.Runner {
   813  			list = append(list, runner)
   814  		}
   815  		return list
   816  	}
   817  
   818  	// if this is simple run, get the single tool name
   819  	if name := toolName(opt); name != "" {
   820  		return []string{name}
   821  	}
   822  
   823  	return []string{}
   824  }