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