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