sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/github/client.go (about)

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package github
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/rsa"
    23  	"crypto/sha256"
    24  	"encoding/base64"
    25  	"encoding/json"
    26  	"errors"
    27  	"fmt"
    28  	"io"
    29  	"net/http"
    30  	"net/url"
    31  	"regexp"
    32  	"strconv"
    33  	"strings"
    34  	"sync"
    35  	"time"
    36  
    37  	"github.com/prometheus/client_golang/prometheus"
    38  	githubql "github.com/shurcooL/githubv4"
    39  	"github.com/sirupsen/logrus"
    40  	"golang.org/x/oauth2"
    41  	utilerrors "k8s.io/apimachinery/pkg/util/errors"
    42  	"k8s.io/apimachinery/pkg/util/sets"
    43  
    44  	"sigs.k8s.io/prow/pkg/ghcache"
    45  	"sigs.k8s.io/prow/pkg/throttle"
    46  	"sigs.k8s.io/prow/pkg/version"
    47  )
    48  
    49  type timeClient interface {
    50  	Sleep(time.Duration)
    51  	Until(time.Time) time.Duration
    52  }
    53  
    54  type standardTime struct{}
    55  
    56  func (s *standardTime) Sleep(d time.Duration) {
    57  	time.Sleep(d)
    58  }
    59  func (s *standardTime) Until(t time.Time) time.Duration {
    60  	return time.Until(t)
    61  }
    62  
    63  // OrganizationClient interface for organisation related API actions
    64  type OrganizationClient interface {
    65  	IsMember(org, user string) (bool, error)
    66  	GetOrg(name string) (*Organization, error)
    67  	EditOrg(name string, config Organization) (*Organization, error)
    68  	ListOrgInvitations(org string) ([]OrgInvitation, error)
    69  	ListOrgMembers(org, role string) ([]TeamMember, error)
    70  	HasPermission(org, repo, user string, roles ...string) (bool, error)
    71  	GetUserPermission(org, repo, user string) (string, error)
    72  	UpdateOrgMembership(org, user string, admin bool) (*OrgMembership, error)
    73  	RemoveOrgMembership(org, user string) error
    74  }
    75  
    76  // HookClient interface for hook related API actions
    77  type HookClient interface {
    78  	ListOrgHooks(org string) ([]Hook, error)
    79  	ListRepoHooks(org, repo string) ([]Hook, error)
    80  	EditRepoHook(org, repo string, id int, req HookRequest) error
    81  	EditOrgHook(org string, id int, req HookRequest) error
    82  	CreateOrgHook(org string, req HookRequest) (int, error)
    83  	CreateRepoHook(org, repo string, req HookRequest) (int, error)
    84  	DeleteOrgHook(org string, id int, req HookRequest) error
    85  	DeleteRepoHook(org, repo string, id int, req HookRequest) error
    86  	ListCurrentUserRepoInvitations() ([]UserRepoInvitation, error)
    87  	AcceptUserRepoInvitation(invitationID int) error
    88  	ListCurrentUserOrgInvitations() ([]UserOrgInvitation, error)
    89  	AcceptUserOrgInvitation(org string) error
    90  }
    91  
    92  // CommentClient interface for comment related API actions
    93  type CommentClient interface {
    94  	CreateComment(org, repo string, number int, comment string) error
    95  	CreateCommentWithContext(ctx context.Context, org, repo string, number int, comment string) error
    96  	DeleteComment(org, repo string, id int) error
    97  	DeleteCommentWithContext(ctx context.Context, org, repo string, id int) error
    98  	EditComment(org, repo string, id int, comment string) error
    99  	EditCommentWithContext(ctx context.Context, org, repo string, id int, comment string) error
   100  	CreateCommentReaction(org, repo string, id int, reaction string) error
   101  	DeleteStaleComments(org, repo string, number int, comments []IssueComment, isStale func(IssueComment) bool) error
   102  	DeleteStaleCommentsWithContext(ctx context.Context, org, repo string, number int, comments []IssueComment, isStale func(IssueComment) bool) error
   103  }
   104  
   105  // IssueClient interface for issue related API actions
   106  type IssueClient interface {
   107  	CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error)
   108  	CreateIssueReaction(org, repo string, id int, reaction string) error
   109  	ListIssueComments(org, repo string, number int) ([]IssueComment, error)
   110  	ListIssueCommentsWithContext(ctx context.Context, org, repo string, number int) ([]IssueComment, error)
   111  	GetIssueLabels(org, repo string, number int) ([]Label, error)
   112  	ListIssueEvents(org, repo string, num int) ([]ListedIssueEvent, error)
   113  	AssignIssue(org, repo string, number int, logins []string) error
   114  	UnassignIssue(org, repo string, number int, logins []string) error
   115  	CloseIssue(org, repo string, number int) error
   116  	CloseIssueAsNotPlanned(org, repo string, number int) error
   117  	ReopenIssue(org, repo string, number int) error
   118  	FindIssues(query, sort string, asc bool) ([]Issue, error)
   119  	FindIssuesWithOrg(org, query, sort string, asc bool) ([]Issue, error)
   120  	ListOpenIssues(org, repo string) ([]Issue, error)
   121  	GetIssue(org, repo string, number int) (*Issue, error)
   122  	EditIssue(org, repo string, number int, issue *Issue) (*Issue, error)
   123  }
   124  
   125  // PullRequestClient interface for pull request related API actions
   126  type PullRequestClient interface {
   127  	GetPullRequests(org, repo string) ([]PullRequest, error)
   128  	GetPullRequest(org, repo string, number int) (*PullRequest, error)
   129  	EditPullRequest(org, repo string, number int, pr *PullRequest) (*PullRequest, error)
   130  	GetPullRequestDiff(org, repo string, number int) ([]byte, error)
   131  	GetPullRequestPatch(org, repo string, number int) ([]byte, error)
   132  	CreatePullRequest(org, repo, title, body, head, base string, canModify bool) (int, error)
   133  	UpdatePullRequest(org, repo string, number int, title, body *string, open *bool, branch *string, canModify *bool) error
   134  	GetPullRequestChanges(org, repo string, number int) ([]PullRequestChange, error)
   135  	ListPullRequestComments(org, repo string, number int) ([]ReviewComment, error)
   136  	CreatePullRequestReviewComment(org, repo string, number int, rc ReviewComment) error
   137  	ListReviews(org, repo string, number int) ([]Review, error)
   138  	ClosePullRequest(org, repo string, number int) error
   139  	ReopenPullRequest(org, repo string, number int) error
   140  	CreateReview(org, repo string, number int, r DraftReview) error
   141  	RequestReview(org, repo string, number int, logins []string) error
   142  	UnrequestReview(org, repo string, number int, logins []string) error
   143  	Merge(org, repo string, pr int, details MergeDetails) error
   144  	IsMergeable(org, repo string, number int, SHA string) (bool, error)
   145  	ListPullRequestCommits(org, repo string, number int) ([]RepositoryCommit, error)
   146  	UpdatePullRequestBranch(org, repo string, number int, expectedHeadSha *string) error
   147  }
   148  
   149  // CommitClient interface for commit related API actions
   150  type CommitClient interface {
   151  	CreateStatus(org, repo, SHA string, s Status) error
   152  	CreateStatusWithContext(ctx context.Context, org, repo, SHA string, s Status) error
   153  	ListStatuses(org, repo, ref string) ([]Status, error)
   154  	GetSingleCommit(org, repo, SHA string) (RepositoryCommit, error)
   155  	GetCombinedStatus(org, repo, ref string) (*CombinedStatus, error)
   156  	ListCheckRuns(org, repo, ref string) (*CheckRunList, error)
   157  	GetRef(org, repo, ref string) (string, error)
   158  	DeleteRef(org, repo, ref string) error
   159  	ListFileCommits(org, repo, path string) ([]RepositoryCommit, error)
   160  	CreateCheckRun(org, repo string, checkRun CheckRun) error
   161  }
   162  
   163  // RepositoryClient interface for repository related API actions
   164  type RepositoryClient interface {
   165  	GetRepo(owner, name string) (FullRepo, error)
   166  	GetRepos(org string, isUser bool) ([]Repo, error)
   167  	GetBranches(org, repo string, onlyProtected bool) ([]Branch, error)
   168  	GetBranchProtection(org, repo, branch string) (*BranchProtection, error)
   169  	RemoveBranchProtection(org, repo, branch string) error
   170  	UpdateBranchProtection(org, repo, branch string, config BranchProtectionRequest) error
   171  	AddRepoLabel(org, repo, label, description, color string) error
   172  	UpdateRepoLabel(org, repo, label, newName, description, color string) error
   173  	DeleteRepoLabel(org, repo, label string) error
   174  	GetRepoLabels(org, repo string) ([]Label, error)
   175  	AddLabel(org, repo string, number int, label string) error
   176  	AddLabelWithContext(ctx context.Context, org, repo string, number int, label string) error
   177  	AddLabels(org, repo string, number int, labels ...string) error
   178  	AddLabelsWithContext(ctx context.Context, org, repo string, number int, labels ...string) error
   179  	RemoveLabel(org, repo string, number int, label string) error
   180  	RemoveLabelWithContext(ctx context.Context, org, repo string, number int, label string) error
   181  	WasLabelAddedByHuman(org, repo string, number int, label string) (bool, error)
   182  	GetFile(org, repo, filepath, commit string) ([]byte, error)
   183  	GetDirectory(org, repo, dirpath, commit string) ([]DirectoryContent, error)
   184  	IsCollaborator(org, repo, user string) (bool, error)
   185  	ListCollaborators(org, repo string) ([]User, error)
   186  	CreateFork(owner, repo string) (string, error)
   187  	EnsureFork(forkingUser, org, repo string) (string, error)
   188  	ListRepoTeams(org, repo string) ([]Team, error)
   189  	CreateRepo(owner string, isUser bool, repo RepoCreateRequest) (*FullRepo, error)
   190  	UpdateRepo(owner, name string, repo RepoUpdateRequest) (*FullRepo, error)
   191  }
   192  
   193  // TeamClient interface for team related API actions
   194  type TeamClient interface {
   195  	CreateTeam(org string, team Team) (*Team, error)
   196  	EditTeam(org string, t Team) (*Team, error)
   197  	DeleteTeamBySlug(org, teamSlug string) error
   198  	ListTeams(org string) ([]Team, error)
   199  	UpdateTeamMembershipBySlug(org, teamSlug, user string, maintainer bool) (*TeamMembership, error)
   200  	RemoveTeamMembershipBySlug(org, teamSlug, user string) error
   201  	ListTeamMembers(org string, id int, role string) ([]TeamMember, error)
   202  	ListTeamMembersBySlug(org, teamSlug, role string) ([]TeamMember, error)
   203  	ListTeamReposBySlug(org, teamSlug string) ([]Repo, error)
   204  	UpdateTeamRepoBySlug(org, teamSlug, repo string, permission TeamPermission) error
   205  	RemoveTeamRepoBySlug(org, teamSlug, repo string) error
   206  	ListTeamInvitationsBySlug(org, teamSlug string) ([]OrgInvitation, error)
   207  	TeamHasMember(org string, teamID int, memberLogin string) (bool, error)
   208  	TeamBySlugHasMember(org string, teamSlug string, memberLogin string) (bool, error)
   209  	GetTeamBySlug(slug string, org string) (*Team, error)
   210  }
   211  
   212  // UserClient interface for user related API actions
   213  type UserClient interface {
   214  	// BotUser will return details about the user the client runs as. Use BotUserChecker()
   215  	// instead when checking for comment authorship, as the Username in comments might have
   216  	// a [bot] suffix when using github apps authentication.
   217  	BotUser() (*UserData, error)
   218  	// BotUserChecker can be used to check if a comment was authored by the bot user.
   219  	BotUserChecker() (func(candidate string) bool, error)
   220  	BotUserCheckerWithContext(ctx context.Context) (func(candidate string) bool, error)
   221  	Email() (string, error)
   222  }
   223  
   224  // ProjectClient interface for project related API actions
   225  type ProjectClient interface {
   226  	GetRepoProjects(owner, repo string) ([]Project, error)
   227  	GetOrgProjects(org string) ([]Project, error)
   228  	GetProjectColumns(org string, projectID int) ([]ProjectColumn, error)
   229  	CreateProjectCard(org string, columnID int, projectCard ProjectCard) (*ProjectCard, error)
   230  	GetColumnProjectCards(org string, columnID int) ([]ProjectCard, error)
   231  	GetColumnProjectCard(org string, columnID int, issueURL string) (*ProjectCard, error)
   232  	MoveProjectCard(org string, projectCardID int, newColumnID int) error
   233  	DeleteProjectCard(org string, projectCardID int) error
   234  }
   235  
   236  // MilestoneClient interface for milestone related API actions
   237  type MilestoneClient interface {
   238  	ClearMilestone(org, repo string, num int) error
   239  	SetMilestone(org, repo string, issueNum, milestoneNum int) error
   240  	ListMilestones(org, repo string) ([]Milestone, error)
   241  }
   242  
   243  // RerunClient interface for job rerun access check related API actions
   244  type RerunClient interface {
   245  	TeamBySlugHasMember(org string, teamSlug string, memberLogin string) (bool, error)
   246  	TeamHasMember(org string, teamID int, memberLogin string) (bool, error)
   247  	IsCollaborator(org, repo, user string) (bool, error)
   248  	IsMember(org, user string) (bool, error)
   249  	GetIssueLabels(org, repo string, number int) ([]Label, error)
   250  }
   251  
   252  // Client interface for GitHub API
   253  type Client interface {
   254  	PullRequestClient
   255  	RepositoryClient
   256  	CommitClient
   257  	IssueClient
   258  	CommentClient
   259  	OrganizationClient
   260  	TeamClient
   261  	ProjectClient
   262  	MilestoneClient
   263  	UserClient
   264  	HookClient
   265  	ListAppInstallations() ([]AppInstallation, error)
   266  	IsAppInstalled(org, repo string) (bool, error)
   267  	UsesAppAuth() bool
   268  	ListAppInstallationsForOrg(org string) ([]AppInstallation, error)
   269  	GetApp() (*App, error)
   270  	GetAppWithContext(ctx context.Context) (*App, error)
   271  	GetFailedActionRunsByHeadBranch(org, repo, branchName, headSHA string) ([]WorkflowRun, error)
   272  
   273  	Throttle(hourlyTokens, burst int, org ...string) error
   274  	QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error
   275  	MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error
   276  
   277  	SetMax404Retries(int)
   278  
   279  	WithFields(fields logrus.Fields) Client
   280  	ForPlugin(plugin string) Client
   281  	ForSubcomponent(subcomponent string) Client
   282  	Used() bool
   283  	TriggerGitHubWorkflow(org, repo string, id int) error
   284  	TriggerFailedGitHubWorkflow(org, repo string, id int) error
   285  }
   286  
   287  // client interacts with the github api. It is reconstructed whenever
   288  // ForPlugin/ForSubcomment is called to change the Logger and User-Agent
   289  // header, whereas delegate will stay the same.
   290  type client struct {
   291  	// If logger is non-nil, log all method calls with it.
   292  	logger *logrus.Entry
   293  	// identifier is used to add more identification to the user-agent header
   294  	identifier string
   295  	gqlc       gqlClient
   296  	used       bool
   297  	mutUsed    sync.Mutex // protects used
   298  	*delegate
   299  }
   300  
   301  // delegate actually does the work to talk to GitHub
   302  type delegate struct {
   303  	time timeClient
   304  
   305  	maxRetries    int
   306  	max404Retries int
   307  	maxSleepTime  time.Duration
   308  	initialDelay  time.Duration
   309  
   310  	client       httpClient
   311  	bases        []string
   312  	dry          bool
   313  	fake         bool
   314  	usesAppsAuth bool
   315  	throttle     ghThrottler
   316  	getToken     func() []byte
   317  	censor       func([]byte) []byte
   318  
   319  	mut      sync.Mutex // protects botName and email
   320  	userData *UserData
   321  }
   322  
   323  type UserData struct {
   324  	Name  string
   325  	Login string
   326  	Email string
   327  }
   328  
   329  // Used determines whether the client has been used
   330  func (c *client) Used() bool {
   331  	return c.used
   332  }
   333  
   334  // ForPlugin clones the client, keeping the underlying delegate the same but adding
   335  // a plugin identifier and log field
   336  func (c *client) ForPlugin(plugin string) Client {
   337  	return c.forKeyValue("plugin", plugin)
   338  }
   339  
   340  // ForSubcomponent clones the client, keeping the underlying delegate the same but adding
   341  // an identifier and log field
   342  func (c *client) ForSubcomponent(subcomponent string) Client {
   343  	return c.forKeyValue("subcomponent", subcomponent)
   344  }
   345  
   346  func (c *client) forKeyValue(key, value string) Client {
   347  	newClient := &client{
   348  		identifier: value,
   349  		logger:     c.logger.WithField(key, value),
   350  		delegate:   c.delegate,
   351  	}
   352  	newClient.gqlc = c.gqlc.forUserAgent(newClient.userAgent())
   353  	return newClient
   354  }
   355  
   356  func (c *client) userAgent() string {
   357  	if c.identifier != "" {
   358  		return version.UserAgentWithIdentifier(c.identifier)
   359  	}
   360  	return version.UserAgent()
   361  }
   362  
   363  // WithFields clones the client, keeping the underlying delegate the same but adding
   364  // fields to the logging context
   365  func (c *client) WithFields(fields logrus.Fields) Client {
   366  	return &client{
   367  		logger:     c.logger.WithFields(fields),
   368  		identifier: c.identifier,
   369  		gqlc:       c.gqlc,
   370  		delegate:   c.delegate,
   371  	}
   372  }
   373  
   374  var (
   375  	teamRe = regexp.MustCompile(`^(.*)/(.*)$`)
   376  )
   377  
   378  const (
   379  	acceptNone       = ""
   380  	githubApiVersion = "2022-11-28"
   381  
   382  	// MaxRequestTime aborts requests that don't return in 5 mins. Longest graphql
   383  	// calls can take up to 2 minutes. This limit should ensure all successful calls
   384  	// return but will prevent an indefinite stall if GitHub never responds.
   385  	MaxRequestTime = 5 * time.Minute
   386  
   387  	DefaultMaxRetries    = 8
   388  	DefaultMax404Retries = 2
   389  	DefaultMaxSleepTime  = 2 * time.Minute
   390  	DefaultInitialDelay  = 2 * time.Second
   391  )
   392  
   393  // Force the compiler to check if the TokenSource is implementing correctly.
   394  // Tokensource is needed to dynamically update the token in the GraphQL client.
   395  var _ oauth2.TokenSource = &reloadingTokenSource{}
   396  
   397  type reloadingTokenSource struct {
   398  	getToken func() []byte
   399  }
   400  
   401  // Interface for how prow interacts with the http client, which we may throttle.
   402  type httpClient interface {
   403  	Do(req *http.Request) (*http.Response, error)
   404  }
   405  
   406  // Interface for how prow interacts with the graphql client, which we may throttle.
   407  type gqlClient interface {
   408  	QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error
   409  	MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error
   410  	forUserAgent(userAgent string) gqlClient
   411  }
   412  
   413  // ghThrottler sets a ceiling on the rate of GitHub requests.
   414  // Configure with Client.Throttle().
   415  // It gets reconstructed whenever forUserAgent() is called,
   416  // whereas its *throttle.Throttler remains.
   417  type ghThrottler struct {
   418  	graph gqlClient
   419  	http  httpClient
   420  	*throttle.Throttler
   421  }
   422  
   423  func (t *ghThrottler) Do(req *http.Request) (*http.Response, error) {
   424  	org := extractOrgFromContext(req.Context())
   425  	if err := t.Wait(req.Context(), org); err != nil {
   426  		return nil, err
   427  	}
   428  	resp, err := t.http.Do(req)
   429  	if err == nil {
   430  		cacheMode := ghcache.CacheResponseMode(resp.Header.Get(ghcache.CacheModeHeader))
   431  		if ghcache.CacheModeIsFree(cacheMode) {
   432  			// This request was fulfilled by ghcache without using an API token.
   433  			// Refund the throttling token we preemptively consumed.
   434  			logrus.WithFields(logrus.Fields{
   435  				"client":     "github",
   436  				"throttled":  true,
   437  				"cache-mode": string(cacheMode),
   438  			}).Debug("Throttler refunding token for free response from ghcache.")
   439  			t.Refund(org)
   440  		} else {
   441  			logrus.WithFields(logrus.Fields{
   442  				"client":     "github",
   443  				"throttled":  true,
   444  				"cache-mode": string(cacheMode),
   445  				"path":       req.URL.Path,
   446  				"method":     req.Method,
   447  			}).Debug("Used token for request")
   448  
   449  		}
   450  	}
   451  	return resp, err
   452  }
   453  
   454  func (t *ghThrottler) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error {
   455  	if err := t.Wait(ctx, extractOrgFromContext(ctx)); err != nil {
   456  		return err
   457  	}
   458  	return t.graph.QueryWithGitHubAppsSupport(ctx, q, vars, org)
   459  }
   460  
   461  func (t *ghThrottler) MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error {
   462  	if err := t.Wait(ctx, extractOrgFromContext(ctx)); err != nil {
   463  		return err
   464  	}
   465  	return t.graph.MutateWithGitHubAppsSupport(ctx, m, input, vars, org)
   466  }
   467  
   468  func (t *ghThrottler) forUserAgent(userAgent string) gqlClient {
   469  	return &ghThrottler{
   470  		graph:     t.graph.forUserAgent(userAgent),
   471  		Throttler: t.Throttler,
   472  	}
   473  }
   474  
   475  // Throttle client to a rate of at most hourlyTokens requests per hour,
   476  // allowing burst tokens.
   477  func (c *client) Throttle(hourlyTokens, burst int, orgs ...string) error {
   478  	if len(orgs) > 0 {
   479  		if !c.usesAppsAuth {
   480  			return errors.New("passing an org to the throttler is only allowed when using github apps auth")
   481  		}
   482  	}
   483  	c.log("Throttle", hourlyTokens, burst, orgs)
   484  	return c.throttle.Throttle(hourlyTokens, burst, orgs...)
   485  }
   486  
   487  func (c *client) SetMax404Retries(max int) {
   488  	c.max404Retries = max
   489  }
   490  
   491  // ClientOptions holds options for creating a new client
   492  type ClientOptions struct {
   493  	// censor knows how to censor output
   494  	Censor func([]byte) []byte
   495  
   496  	// the following fields handle auth
   497  	GetToken      func() []byte
   498  	AppID         string
   499  	AppPrivateKey func() *rsa.PrivateKey
   500  
   501  	// the following fields determine which server we talk to
   502  	GraphqlEndpoint string
   503  	Bases           []string
   504  
   505  	// the following fields determine client retry behavior
   506  	MaxRequestTime, InitialDelay, MaxSleepTime time.Duration
   507  	MaxRetries, Max404Retries                  int
   508  
   509  	DryRun bool
   510  	// BaseRoundTripper is the last RoundTripper to be called. Used for testing, gets defaulted to http.DefaultTransport
   511  	BaseRoundTripper http.RoundTripper
   512  }
   513  
   514  func (o ClientOptions) Default() ClientOptions {
   515  	if o.MaxRequestTime == 0 {
   516  		o.MaxRequestTime = MaxRequestTime
   517  	}
   518  	if o.InitialDelay == 0 {
   519  		o.InitialDelay = DefaultInitialDelay
   520  	}
   521  	if o.MaxSleepTime == 0 {
   522  		o.MaxSleepTime = DefaultMaxSleepTime
   523  	}
   524  	if o.MaxRetries == 0 {
   525  		o.MaxRetries = DefaultMaxRetries
   526  	}
   527  	if o.Max404Retries == 0 {
   528  		o.Max404Retries = DefaultMax404Retries
   529  	}
   530  	return o
   531  }
   532  
   533  // TokenGenerator knows how to generate a token for use in git client calls
   534  type TokenGenerator func(org string) (string, error)
   535  
   536  // UserGenerator knows how to identify this user for use in git client calls
   537  type UserGenerator func() (string, error)
   538  
   539  // NewClientWithFields creates a new fully operational GitHub client. With
   540  // added logging fields.
   541  // 'getToken' is a generator for the GitHub access token to use.
   542  // 'bases' is a variadic slice of endpoints to use in order of preference.
   543  //
   544  //	An endpoint is used when all preceding endpoints have returned a conn err.
   545  //	This should be used when using the ghproxy GitHub proxy cache to allow
   546  //	this client to bypass the cache if it is temporarily unavailable.
   547  func NewClientWithFields(fields logrus.Fields, getToken func() []byte, censor func([]byte) []byte, graphqlEndpoint string, bases ...string) (Client, error) {
   548  	_, _, client, err := NewClientFromOptions(fields, ClientOptions{
   549  		Censor:          censor,
   550  		GetToken:        getToken,
   551  		GraphqlEndpoint: graphqlEndpoint,
   552  		Bases:           bases,
   553  		DryRun:          false,
   554  	}.Default())
   555  	return client, err
   556  }
   557  
   558  func NewAppsAuthClientWithFields(fields logrus.Fields, censor func([]byte) []byte, appID string, appPrivateKey func() *rsa.PrivateKey, graphqlEndpoint string, bases ...string) (TokenGenerator, UserGenerator, Client, error) {
   559  	return NewClientFromOptions(fields, ClientOptions{
   560  		Censor:          censor,
   561  		AppID:           appID,
   562  		AppPrivateKey:   appPrivateKey,
   563  		GraphqlEndpoint: graphqlEndpoint,
   564  		Bases:           bases,
   565  		DryRun:          false,
   566  	}.Default())
   567  }
   568  
   569  // This should only be called once when the client is created.
   570  func (c *client) wrapThrottler() {
   571  	c.throttle.http = c.client
   572  	c.throttle.graph = c.gqlc
   573  	c.client = &c.throttle
   574  	c.gqlc = &c.throttle
   575  }
   576  
   577  // NewClientFromOptions creates a new client from the options we expose. This method should be used over the more-specific ones.
   578  func NewClientFromOptions(fields logrus.Fields, options ClientOptions) (TokenGenerator, UserGenerator, Client, error) {
   579  	options = options.Default()
   580  
   581  	// Will be nil if github app authentication is used
   582  	if options.GetToken == nil {
   583  		options.GetToken = func() []byte { return nil }
   584  	}
   585  	if options.BaseRoundTripper == nil {
   586  		options.BaseRoundTripper = http.DefaultTransport
   587  	}
   588  
   589  	httpClient := &http.Client{
   590  		Transport: options.BaseRoundTripper,
   591  		Timeout:   options.MaxRequestTime,
   592  	}
   593  	graphQLTransport := newAddHeaderTransport(options.BaseRoundTripper)
   594  	c := &client{
   595  		logger: logrus.WithFields(fields).WithField("client", "github"),
   596  		gqlc: &graphQLGitHubAppsAuthClientWrapper{Client: githubql.NewEnterpriseClient(
   597  			options.GraphqlEndpoint,
   598  			&http.Client{
   599  				Timeout: options.MaxRequestTime,
   600  				Transport: &oauth2.Transport{
   601  					Source: newReloadingTokenSource(options.GetToken),
   602  					Base:   graphQLTransport,
   603  				},
   604  			})},
   605  		delegate: &delegate{
   606  			time:          &standardTime{},
   607  			client:        httpClient,
   608  			bases:         options.Bases,
   609  			throttle:      ghThrottler{Throttler: &throttle.Throttler{}},
   610  			getToken:      options.GetToken,
   611  			censor:        options.Censor,
   612  			dry:           options.DryRun,
   613  			usesAppsAuth:  options.AppID != "",
   614  			maxRetries:    options.MaxRetries,
   615  			max404Retries: options.Max404Retries,
   616  			initialDelay:  options.InitialDelay,
   617  			maxSleepTime:  options.MaxSleepTime,
   618  		},
   619  	}
   620  	c.gqlc = c.gqlc.forUserAgent(c.userAgent())
   621  
   622  	// Wrap clients with the throttler
   623  	c.wrapThrottler()
   624  
   625  	var tokenGenerator func(_ string) (string, error)
   626  	var userGenerator func() (string, error)
   627  	if options.AppID != "" {
   628  		appsTransport, err := newAppsRoundTripper(options.AppID, options.AppPrivateKey, options.BaseRoundTripper, c, options.Bases)
   629  		if err != nil {
   630  			return nil, nil, nil, fmt.Errorf("failed to construct apps auth roundtripper: %w", err)
   631  		}
   632  		httpClient.Transport = appsTransport
   633  		graphQLTransport.upstream = appsTransport
   634  
   635  		// Use github apps auth for git actions
   636  		// https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps#http-based-git-access-by-an-installation=
   637  		tokenGenerator = func(org string) (string, error) {
   638  			res, _, err := appsTransport.installationTokenFor(org)
   639  			return res, err
   640  		}
   641  		userGenerator = func() (string, error) {
   642  			return "x-access-token", nil
   643  		}
   644  	} else {
   645  		// Use Personal Access token auth for git actions
   646  		tokenGenerator = func(_ string) (string, error) {
   647  			return string(options.GetToken()), nil
   648  		}
   649  		userGenerator = func() (string, error) {
   650  			user, err := c.BotUser()
   651  			if err != nil {
   652  				return "", err
   653  			}
   654  			return user.Login, nil
   655  		}
   656  	}
   657  
   658  	return tokenGenerator, userGenerator, c, nil
   659  }
   660  
   661  type graphQLGitHubAppsAuthClientWrapper struct {
   662  	*githubql.Client
   663  	userAgent string
   664  }
   665  
   666  type contextKey int
   667  
   668  const (
   669  	userAgentContextKey contextKey = iota
   670  	githubOrgContextKey
   671  )
   672  
   673  func (c *graphQLGitHubAppsAuthClientWrapper) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error {
   674  	ctx = context.WithValue(ctx, githubOrgContextKey, org)
   675  	ctx = context.WithValue(ctx, userAgentContextKey, c.userAgent)
   676  	return c.Client.Query(ctx, q, vars)
   677  }
   678  
   679  func (c *graphQLGitHubAppsAuthClientWrapper) MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error {
   680  	ctx = context.WithValue(ctx, githubOrgContextKey, org)
   681  	ctx = context.WithValue(ctx, userAgentContextKey, c.userAgent)
   682  	return c.Client.Mutate(ctx, m, input, vars)
   683  }
   684  
   685  func (c *graphQLGitHubAppsAuthClientWrapper) forUserAgent(userAgent string) gqlClient {
   686  	return &graphQLGitHubAppsAuthClientWrapper{
   687  		Client:    c.Client,
   688  		userAgent: userAgent,
   689  	}
   690  }
   691  
   692  // addHeaderTransport implements http.RoundTripper
   693  var _ http.RoundTripper = &addHeaderTransport{}
   694  
   695  func newAddHeaderTransport(upstream http.RoundTripper) *addHeaderTransport {
   696  	return &addHeaderTransport{upstream}
   697  }
   698  
   699  type addHeaderTransport struct {
   700  	upstream http.RoundTripper
   701  }
   702  
   703  func (s *addHeaderTransport) RoundTrip(r *http.Request) (*http.Response, error) {
   704  	// We have to add this header to enable the Checks scheme preview:
   705  	// https://docs.github.com/en/enterprise-server@2.22/graphql/overview/schema-previews
   706  	// Any GHE version after 2.22 will enable the Checks types per default
   707  	r.Header.Add("Accept", "application/vnd.github.antiope-preview+json")
   708  
   709  	// We have to add this header to enable the Merge info scheme preview:
   710  	// https://docs.github.com/en/graphql/overview/schema-previews#merge-info-preview
   711  	r.Header.Add("Accept", "application/vnd.github.merge-info-preview+json")
   712  
   713  	// We use the context to pass the UserAgent through the V4 client we depend on
   714  	if v := r.Context().Value(userAgentContextKey); v != nil {
   715  		r.Header.Add("User-Agent", v.(string))
   716  	}
   717  
   718  	return s.upstream.RoundTrip(r)
   719  }
   720  
   721  // NewClient creates a new fully operational GitHub client.
   722  func NewClient(getToken func() []byte, censor func([]byte) []byte, graphqlEndpoint string, bases ...string) (Client, error) {
   723  	return NewClientWithFields(logrus.Fields{}, getToken, censor, graphqlEndpoint, bases...)
   724  }
   725  
   726  // NewDryRunClientWithFields creates a new client that will not perform mutating actions
   727  // such as setting statuses or commenting, but it will still query GitHub and
   728  // use up API tokens. Additional fields are added to the logger.
   729  // 'getToken' is a generator the GitHub access token to use.
   730  // 'bases' is a variadic slice of endpoints to use in order of preference.
   731  //
   732  //	An endpoint is used when all preceding endpoints have returned a conn err.
   733  //	This should be used when using the ghproxy GitHub proxy cache to allow
   734  //	this client to bypass the cache if it is temporarily unavailable.
   735  func NewDryRunClientWithFields(fields logrus.Fields, getToken func() []byte, censor func([]byte) []byte, graphqlEndpoint string, bases ...string) (Client, error) {
   736  	_, _, client, err := NewClientFromOptions(fields, ClientOptions{
   737  		Censor:          censor,
   738  		GetToken:        getToken,
   739  		GraphqlEndpoint: graphqlEndpoint,
   740  		Bases:           bases,
   741  		DryRun:          true,
   742  	}.Default())
   743  	return client, err
   744  }
   745  
   746  // NewAppsAuthDryRunClientWithFields creates a new client that will not perform mutating actions
   747  // such as setting statuses or commenting, but it will still query GitHub and
   748  // use up API tokens. Additional fields are added to the logger.
   749  func NewAppsAuthDryRunClientWithFields(fields logrus.Fields, censor func([]byte) []byte, appId string, appPrivateKey func() *rsa.PrivateKey, graphqlEndpoint string, bases ...string) (TokenGenerator, UserGenerator, Client, error) {
   750  	return NewClientFromOptions(fields, ClientOptions{
   751  		Censor:          censor,
   752  		AppID:           appId,
   753  		AppPrivateKey:   appPrivateKey,
   754  		GraphqlEndpoint: graphqlEndpoint,
   755  		Bases:           bases,
   756  		DryRun:          false,
   757  	}.Default())
   758  }
   759  
   760  // NewDryRunClient creates a new client that will not perform mutating actions
   761  // such as setting statuses or commenting, but it will still query GitHub and
   762  // use up API tokens.
   763  // 'getToken' is a generator the GitHub access token to use.
   764  // 'bases' is a variadic slice of endpoints to use in order of preference.
   765  //
   766  //	An endpoint is used when all preceding endpoints have returned a conn err.
   767  //	This should be used when using the ghproxy GitHub proxy cache to allow
   768  //	this client to bypass the cache if it is temporarily unavailable.
   769  func NewDryRunClient(getToken func() []byte, censor func([]byte) []byte, graphqlEndpoint string, bases ...string) (Client, error) {
   770  	return NewDryRunClientWithFields(logrus.Fields{}, getToken, censor, graphqlEndpoint, bases...)
   771  }
   772  
   773  // NewFakeClient creates a new client that will not perform any actions at all.
   774  func NewFakeClient() Client {
   775  	return &client{
   776  		logger: logrus.WithField("client", "github"),
   777  		gqlc:   &graphQLGitHubAppsAuthClientWrapper{},
   778  		delegate: &delegate{
   779  			time: &standardTime{},
   780  			fake: true,
   781  			dry:  true,
   782  		},
   783  	}
   784  }
   785  
   786  func (c *client) log(methodName string, args ...interface{}) (logDuration func()) {
   787  	c.mutUsed.Lock()
   788  	c.used = true
   789  	c.mutUsed.Unlock()
   790  
   791  	if c.logger == nil {
   792  		return func() {}
   793  	}
   794  	var as []string
   795  	for _, arg := range args {
   796  		as = append(as, fmt.Sprintf("%v", arg))
   797  	}
   798  	start := time.Now()
   799  	c.logger.Infof("%s(%s)", methodName, strings.Join(as, ", "))
   800  	return func() {
   801  		c.logger.WithField("duration", time.Since(start).String()).Debugf("%s(%s) finished", methodName, strings.Join(as, ", "))
   802  	}
   803  }
   804  
   805  type request struct {
   806  	method      string
   807  	path        string
   808  	accept      string
   809  	org         string
   810  	requestBody interface{}
   811  	exitCodes   []int
   812  }
   813  
   814  type requestError struct {
   815  	StatusCode  int
   816  	ClientError error
   817  	ErrorString string
   818  }
   819  
   820  func (r requestError) Error() string {
   821  	return r.ErrorString
   822  }
   823  
   824  func (r requestError) ErrorMessages() []string {
   825  	clientErr, isClientError := r.ClientError.(ClientError)
   826  	if isClientError {
   827  		errors := []string{}
   828  		for _, subErr := range clientErr.Errors {
   829  			errors = append(errors, subErr.Message)
   830  		}
   831  		return errors
   832  	}
   833  	alternativeClientErr, isAlternativeClientError := r.ClientError.(AlternativeClientError)
   834  	if isAlternativeClientError {
   835  		return alternativeClientErr.Errors
   836  	}
   837  	return []string{}
   838  }
   839  
   840  // NewNotFound returns a NotFound error which may be useful for tests
   841  func NewNotFound() error {
   842  	return requestError{
   843  		ClientError: ClientError{
   844  			Errors: []clientErrorSubError{{Message: "status code 404"}},
   845  		},
   846  	}
   847  }
   848  
   849  func IsNotFound(err error) bool {
   850  	if err == nil {
   851  		return false
   852  	}
   853  
   854  	var requestErr requestError
   855  	if !errors.As(err, &requestErr) {
   856  		return false
   857  	}
   858  
   859  	if requestErr.StatusCode == http.StatusNotFound {
   860  		return true
   861  	}
   862  
   863  	for _, errorMsg := range requestErr.ErrorMessages() {
   864  		if strings.Contains(errorMsg, "status code 404") {
   865  			return true
   866  		}
   867  	}
   868  	return false
   869  }
   870  
   871  // Make a request with retries. If ret is not nil, unmarshal the response body
   872  // into it. Returns an error if the exit code is not one of the provided codes.
   873  func (c *client) request(r *request, ret interface{}) (int, error) {
   874  	return c.requestWithContext(context.Background(), r, ret)
   875  }
   876  
   877  func (c *client) requestWithContext(ctx context.Context, r *request, ret interface{}) (int, error) {
   878  	statusCode, b, err := c.requestRawWithContext(ctx, r)
   879  	if err != nil {
   880  		return statusCode, err
   881  	}
   882  	if ret != nil {
   883  		if err := json.Unmarshal(b, ret); err != nil {
   884  			return statusCode, err
   885  		}
   886  	}
   887  	return statusCode, nil
   888  }
   889  
   890  // requestRaw makes a request with retries and returns the response body.
   891  // Returns an error if the exit code is not one of the provided codes.
   892  func (c *client) requestRaw(r *request) (int, []byte, error) {
   893  	return c.requestRawWithContext(context.Background(), r)
   894  }
   895  
   896  func (c *client) requestRawWithContext(ctx context.Context, r *request) (int, []byte, error) {
   897  	if c.fake || (c.dry && r.method != http.MethodGet) {
   898  		return r.exitCodes[0], nil, nil
   899  	}
   900  	resp, err := c.requestRetryWithContext(ctx, r.method, r.path, r.accept, r.org, r.requestBody)
   901  	if err != nil {
   902  		return 0, nil, err
   903  	}
   904  	defer resp.Body.Close()
   905  	b, err := io.ReadAll(resp.Body)
   906  	if err != nil {
   907  		return 0, nil, err
   908  	}
   909  	var okCode bool
   910  	for _, code := range r.exitCodes {
   911  		if code == resp.StatusCode {
   912  			okCode = true
   913  			break
   914  		}
   915  	}
   916  	if !okCode {
   917  		clientError := unmarshalClientError(b)
   918  		err = requestError{
   919  			StatusCode:  resp.StatusCode,
   920  			ClientError: clientError,
   921  			ErrorString: fmt.Sprintf("status code %d not one of %v, body: %s", resp.StatusCode, r.exitCodes, string(b)),
   922  		}
   923  	}
   924  	return resp.StatusCode, b, err
   925  }
   926  
   927  // Retry on transport failures. Retries on 500s, retries after sleep on
   928  // ratelimit exceeded, and retries 404s a couple times.
   929  // This function closes the response body iff it also returns an error.
   930  func (c *client) requestRetry(method, path, accept, org string, body interface{}) (*http.Response, error) {
   931  	return c.requestRetryWithContext(context.Background(), method, path, accept, org, body)
   932  }
   933  
   934  func (c *client) requestRetryWithContext(ctx context.Context, method, path, accept, org string, body interface{}) (*http.Response, error) {
   935  	var hostIndex int
   936  	var resp *http.Response
   937  	var err error
   938  	backoff := c.initialDelay
   939  	for retries := 0; retries < c.maxRetries; retries++ {
   940  		if retries > 0 && resp != nil {
   941  			resp.Body.Close()
   942  		}
   943  		resp, err = c.doRequest(ctx, method, c.bases[hostIndex]+path, accept, org, body)
   944  		if err == nil {
   945  			if resp.StatusCode == 404 && retries < c.max404Retries {
   946  				// Retry 404s a couple times. Sometimes GitHub is inconsistent in
   947  				// the sense that they send us an event such as "PR opened" but an
   948  				// immediate request to GET the PR returns 404. We don't want to
   949  				// retry more than a couple times in this case, because a 404 may
   950  				// be caused by a bad API call and we'll just burn through API
   951  				// tokens.
   952  				c.logger.WithField("backoff", backoff.String()).Debug("Retrying 404")
   953  				c.time.Sleep(backoff)
   954  				backoff *= 2
   955  			} else if resp.StatusCode == 403 {
   956  				if resp.Header.Get("X-RateLimit-Remaining") == "0" {
   957  					// If we are out of API tokens, sleep first. The X-RateLimit-Reset
   958  					// header tells us the time at which we can request again.
   959  					var t int
   960  					if t, err = strconv.Atoi(resp.Header.Get("X-RateLimit-Reset")); err == nil {
   961  						// Sleep an extra second plus how long GitHub wants us to
   962  						// sleep. If it's going to take too long, then break.
   963  						sleepTime := c.time.Until(time.Unix(int64(t), 0)) + time.Second
   964  						if sleepTime < c.maxSleepTime {
   965  							c.logger.WithField("backoff", sleepTime.String()).WithField("path", path).Debug("Retrying after token budget reset")
   966  							c.time.Sleep(sleepTime)
   967  						} else {
   968  							err = fmt.Errorf("sleep time for token reset exceeds max sleep time (%v > %v)", sleepTime, c.maxSleepTime)
   969  							resp.Body.Close()
   970  							break
   971  						}
   972  					} else {
   973  						err = fmt.Errorf("failed to parse rate limit reset unix time %q: %w", resp.Header.Get("X-RateLimit-Reset"), err)
   974  						resp.Body.Close()
   975  						break
   976  					}
   977  				} else if rawTime := resp.Header.Get("Retry-After"); rawTime != "" && rawTime != "0" {
   978  					// If we are getting abuse rate limited, we need to wait or
   979  					// else we risk continuing to make the situation worse
   980  					var t int
   981  					if t, err = strconv.Atoi(rawTime); err == nil {
   982  						// Sleep an extra second plus how long GitHub wants us to
   983  						// sleep. If it's going to take too long, then break.
   984  						sleepTime := time.Duration(t+1) * time.Second
   985  						if sleepTime < c.maxSleepTime {
   986  							c.logger.WithField("backoff", sleepTime.String()).WithField("path", path).Debug("Retrying after abuse ratelimit reset")
   987  							c.time.Sleep(sleepTime)
   988  						} else {
   989  							err = fmt.Errorf("sleep time for abuse rate limit exceeds max sleep time (%v > %v)", sleepTime, c.maxSleepTime)
   990  							resp.Body.Close()
   991  							break
   992  						}
   993  					} else {
   994  						err = fmt.Errorf("failed to parse abuse rate limit wait time %q: %w", rawTime, err)
   995  						resp.Body.Close()
   996  						break
   997  					}
   998  				} else {
   999  					acceptedScopes := resp.Header.Get("X-Accepted-OAuth-Scopes")
  1000  					authorizedScopes := resp.Header.Get("X-OAuth-Scopes")
  1001  					if authorizedScopes == "" {
  1002  						authorizedScopes = "no"
  1003  					}
  1004  
  1005  					want := sets.New[string]()
  1006  					for _, acceptedScope := range strings.Split(acceptedScopes, ",") {
  1007  						want.Insert(strings.TrimSpace(acceptedScope))
  1008  					}
  1009  					var got []string
  1010  					for _, authorizedScope := range strings.Split(authorizedScopes, ",") {
  1011  						got = append(got, strings.TrimSpace(authorizedScope))
  1012  					}
  1013  					if acceptedScopes != "" && !want.HasAny(got...) {
  1014  						err = fmt.Errorf("the account is using %s oauth scopes, please make sure you are using at least one of the following oauth scopes: %s", authorizedScopes, acceptedScopes)
  1015  					} else {
  1016  						body, _ := io.ReadAll(resp.Body)
  1017  						err = fmt.Errorf("the GitHub API request returns a 403 error: %s", string(body))
  1018  					}
  1019  					resp.Body.Close()
  1020  					break
  1021  				}
  1022  			} else if resp.StatusCode < 500 {
  1023  				// Normal, happy case.
  1024  				break
  1025  			} else {
  1026  				// Retry 500 after a break.
  1027  				c.logger.WithField("backoff", backoff.String()).Debug("Retrying 5XX")
  1028  				c.time.Sleep(backoff)
  1029  				backoff *= 2
  1030  			}
  1031  		} else if errors.Is(err, &appsAuthError{}) {
  1032  			c.logger.WithError(err).Error("Stopping retry due to appsAuthError")
  1033  			return resp, err
  1034  		} else if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
  1035  			return resp, err
  1036  		} else {
  1037  			// Connection problem. Try a different host.
  1038  			oldHostIndex := hostIndex
  1039  			hostIndex = (hostIndex + 1) % len(c.bases)
  1040  			c.logger.WithFields(logrus.Fields{
  1041  				"err":          err,
  1042  				"backoff":      backoff.String(),
  1043  				"old-endpoint": c.bases[oldHostIndex],
  1044  				"new-endpoint": c.bases[hostIndex],
  1045  			}).Debug("Retrying request due to connection problem")
  1046  			c.time.Sleep(backoff)
  1047  			backoff *= 2
  1048  		}
  1049  	}
  1050  	return resp, err
  1051  }
  1052  
  1053  func (c *client) doRequest(ctx context.Context, method, path, accept, org string, body interface{}) (*http.Response, error) {
  1054  	var buf io.Reader
  1055  	if body != nil {
  1056  		b, err := json.Marshal(body)
  1057  		if err != nil {
  1058  			return nil, err
  1059  		}
  1060  		b = c.censor(b)
  1061  		buf = bytes.NewBuffer(b)
  1062  	}
  1063  	req, err := http.NewRequestWithContext(ctx, method, path, buf)
  1064  	if err != nil {
  1065  		return nil, fmt.Errorf("failed creating new request: %w", err)
  1066  	}
  1067  	// We do not make use of the Set() method to set this header because
  1068  	// the header name `X-GitHub-Api-Version` is non-canonical in nature.
  1069  	//
  1070  	// See https://pkg.go.dev/net/http#Header.Set for more info.
  1071  	req.Header["X-GitHub-Api-Version"] = []string{githubApiVersion}
  1072  	c.logger.Debugf("Using GitHub REST API Version: %s", githubApiVersion)
  1073  	if header := c.authHeader(); len(header) > 0 {
  1074  		req.Header.Set("Authorization", header)
  1075  	}
  1076  	if accept == acceptNone {
  1077  		req.Header.Add("Accept", "application/vnd.github.v3+json")
  1078  	} else {
  1079  		req.Header.Add("Accept", accept)
  1080  	}
  1081  	if userAgent := c.userAgent(); userAgent != "" {
  1082  		req.Header.Add("User-Agent", userAgent)
  1083  	}
  1084  	if org != "" {
  1085  		req = req.WithContext(context.WithValue(req.Context(), githubOrgContextKey, org))
  1086  	}
  1087  	// Disable keep-alive so that we don't get flakes when GitHub closes the
  1088  	// connection prematurely.
  1089  	// https://go-review.googlesource.com/#/c/3210/ fixed it for GET, but not
  1090  	// for POST.
  1091  	req.Close = true
  1092  
  1093  	c.logger.WithField("curl", toCurl(req)).Trace("Executing http request")
  1094  	return c.client.Do(req)
  1095  }
  1096  
  1097  // toCurl is a slightly adjusted copy of https://github.com/kubernetes/kubernetes/blob/74053d555d71a14e3853b97e204d7d6415521375/staging/src/k8s.io/client-go/transport/round_trippers.go#L339
  1098  func toCurl(r *http.Request) string {
  1099  	headers := ""
  1100  	for key, values := range r.Header {
  1101  		for _, value := range values {
  1102  			headers += fmt.Sprintf(` -H %q`, fmt.Sprintf("%s: %s", key, maskAuthorizationHeader(key, value)))
  1103  		}
  1104  	}
  1105  
  1106  	return fmt.Sprintf("curl -k -v -X%s %s '%s'", r.Method, headers, r.URL.String())
  1107  }
  1108  
  1109  var knownAuthTypes = sets.New[string]("bearer", "basic", "negotiate")
  1110  
  1111  // maskAuthorizationHeader masks credential content from authorization headers
  1112  // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
  1113  func maskAuthorizationHeader(key string, value string) string {
  1114  	if !strings.EqualFold(key, "Authorization") {
  1115  		return value
  1116  	}
  1117  	if len(value) == 0 {
  1118  		return ""
  1119  	}
  1120  	var authType string
  1121  	if i := strings.Index(value, " "); i > 0 {
  1122  		authType = value[0:i]
  1123  	} else {
  1124  		authType = value
  1125  	}
  1126  	if !knownAuthTypes.Has(strings.ToLower(authType)) {
  1127  		return "<masked>"
  1128  	}
  1129  	if len(value) > len(authType)+1 {
  1130  		value = authType + " <masked>"
  1131  	} else {
  1132  		value = authType
  1133  	}
  1134  	return value
  1135  }
  1136  
  1137  func (c *client) authHeader() string {
  1138  	if c.getToken == nil {
  1139  		return ""
  1140  	}
  1141  	token := c.getToken()
  1142  	if len(token) == 0 {
  1143  		return ""
  1144  	}
  1145  	return fmt.Sprintf("Bearer %s", token)
  1146  }
  1147  
  1148  // userInfo provides the 'github_user_info' vector that is indexed
  1149  // by the user's information.
  1150  var userInfo = prometheus.NewGaugeVec(
  1151  	prometheus.GaugeOpts{
  1152  		Name: "github_user_info",
  1153  		Help: "Metadata about a user, tied to their token hash.",
  1154  	},
  1155  	[]string{"token_hash", "login", "email"},
  1156  )
  1157  
  1158  func init() {
  1159  	prometheus.MustRegister(userInfo)
  1160  }
  1161  
  1162  // Not thread-safe - callers need to hold c.mut.
  1163  func (c *client) getUserData(ctx context.Context) error {
  1164  	if c.delegate.usesAppsAuth {
  1165  		resp, err := c.GetAppWithContext(ctx)
  1166  		if err != nil {
  1167  			return err
  1168  		}
  1169  		c.userData = &UserData{
  1170  			Name:  resp.Name,
  1171  			Login: resp.Slug,
  1172  			Email: fmt.Sprintf("%s@users.noreply.github.com", resp.Slug),
  1173  		}
  1174  		return nil
  1175  	}
  1176  	c.log("User")
  1177  	var u User
  1178  	_, err := c.requestWithContext(ctx, &request{
  1179  		method:    http.MethodGet,
  1180  		path:      "/user",
  1181  		exitCodes: []int{200},
  1182  	}, &u)
  1183  	if err != nil {
  1184  		return err
  1185  	}
  1186  	c.userData = &UserData{
  1187  		Name:  u.Name,
  1188  		Login: u.Login,
  1189  		Email: u.Email,
  1190  	}
  1191  	// email needs to be publicly accessible via the profile
  1192  	// of the current account. Read below for more info
  1193  	// https://developer.github.com/v3/users/#get-a-single-user
  1194  
  1195  	// record information for the user
  1196  	authHeaderHash := fmt.Sprintf("%x", sha256.Sum256([]byte(c.authHeader()))) // use %x to make this a utf-8 string for use as a label
  1197  	userInfo.With(prometheus.Labels{"token_hash": authHeaderHash, "login": c.userData.Login, "email": c.userData.Email}).Set(1)
  1198  	return nil
  1199  }
  1200  
  1201  // BotUser returns the user data of the authenticated identity.
  1202  //
  1203  // See https://developer.github.com/v3/users/#get-the-authenticated-user
  1204  func (c *client) BotUser() (*UserData, error) {
  1205  	c.mut.Lock()
  1206  	defer c.mut.Unlock()
  1207  	if c.userData == nil {
  1208  		if err := c.getUserData(context.Background()); err != nil {
  1209  			return nil, fmt.Errorf("fetching bot name from GitHub: %w", err)
  1210  		}
  1211  	}
  1212  	return c.userData, nil
  1213  }
  1214  
  1215  func (c *client) BotUserChecker() (func(candidate string) bool, error) {
  1216  	return c.BotUserCheckerWithContext(context.Background())
  1217  }
  1218  
  1219  func (c *client) BotUserCheckerWithContext(ctx context.Context) (func(candidate string) bool, error) {
  1220  	c.mut.Lock()
  1221  	defer c.mut.Unlock()
  1222  	if c.userData == nil {
  1223  		if err := c.getUserData(ctx); err != nil {
  1224  			return nil, fmt.Errorf("fetching userdata from GitHub: %w", err)
  1225  		}
  1226  	}
  1227  
  1228  	botUser := c.userData.Login
  1229  	return func(candidate string) bool {
  1230  		if c.usesAppsAuth {
  1231  			candidate = strings.TrimSuffix(candidate, "[bot]")
  1232  		}
  1233  		return candidate == botUser
  1234  	}, nil
  1235  }
  1236  
  1237  // Email returns the user-configured email for the authenticated identity.
  1238  //
  1239  // See https://developer.github.com/v3/users/#get-the-authenticated-user
  1240  func (c *client) Email() (string, error) {
  1241  	c.mut.Lock()
  1242  	defer c.mut.Unlock()
  1243  	if c.userData == nil {
  1244  		if err := c.getUserData(context.Background()); err != nil {
  1245  			return "", fmt.Errorf("fetching e-mail from GitHub: %w", err)
  1246  		}
  1247  	}
  1248  	return c.userData.Email, nil
  1249  }
  1250  
  1251  // IsMember returns whether or not the user is a member of the org.
  1252  //
  1253  // See https://developer.github.com/v3/orgs/members/#check-membership
  1254  func (c *client) IsMember(org, user string) (bool, error) {
  1255  	c.log("IsMember", org, user)
  1256  	if org == user {
  1257  		// Make it possible to run a couple of plugins on personal repos.
  1258  		return true, nil
  1259  	}
  1260  	code, err := c.request(&request{
  1261  		method:    http.MethodGet,
  1262  		path:      fmt.Sprintf("/orgs/%s/members/%s", org, user),
  1263  		org:       org,
  1264  		exitCodes: []int{204, 404, 302},
  1265  	}, nil)
  1266  	if err != nil {
  1267  		return false, err
  1268  	}
  1269  	if code == 204 {
  1270  		return true, nil
  1271  	} else if code == 404 {
  1272  		return false, nil
  1273  	} else if code == 302 {
  1274  		return false, fmt.Errorf("requester is not %s org member", org)
  1275  	}
  1276  	// Should be unreachable.
  1277  	return false, fmt.Errorf("unexpected status: %d", code)
  1278  }
  1279  
  1280  func (c *client) listHooks(org string, repo *string) ([]Hook, error) {
  1281  	var ret []Hook
  1282  	var path string
  1283  	if repo != nil {
  1284  		path = fmt.Sprintf("/repos/%s/%s/hooks", org, *repo)
  1285  	} else {
  1286  		path = fmt.Sprintf("/orgs/%s/hooks", org)
  1287  	}
  1288  	err := c.readPaginatedResults(
  1289  		path,
  1290  		acceptNone,
  1291  		org,
  1292  		func() interface{} {
  1293  			return &[]Hook{}
  1294  		},
  1295  		func(obj interface{}) {
  1296  			ret = append(ret, *(obj.(*[]Hook))...)
  1297  		},
  1298  	)
  1299  	if err != nil {
  1300  		return nil, err
  1301  	}
  1302  	return ret, nil
  1303  }
  1304  
  1305  // ListOrgHooks returns a list of hooks for the org.
  1306  // https://developer.github.com/v3/orgs/hooks/#list-hooks
  1307  func (c *client) ListOrgHooks(org string) ([]Hook, error) {
  1308  	c.log("ListOrgHooks", org)
  1309  	return c.listHooks(org, nil)
  1310  }
  1311  
  1312  // ListRepoHooks returns a list of hooks for the repo.
  1313  // https://developer.github.com/v3/repos/hooks/#list-hooks
  1314  func (c *client) ListRepoHooks(org, repo string) ([]Hook, error) {
  1315  	c.log("ListRepoHooks", org, repo)
  1316  	return c.listHooks(org, &repo)
  1317  }
  1318  
  1319  func (c *client) editHook(org string, repo *string, id int, req HookRequest) error {
  1320  	if c.dry {
  1321  		return nil
  1322  	}
  1323  	var path string
  1324  	if repo != nil {
  1325  		path = fmt.Sprintf("/repos/%s/%s/hooks/%d", org, *repo, id)
  1326  	} else {
  1327  		path = fmt.Sprintf("/orgs/%s/hooks/%d", org, id)
  1328  	}
  1329  
  1330  	_, err := c.request(&request{
  1331  		method:      http.MethodPatch,
  1332  		path:        path,
  1333  		org:         org,
  1334  		exitCodes:   []int{200},
  1335  		requestBody: &req,
  1336  	}, nil)
  1337  	return err
  1338  }
  1339  
  1340  // EditRepoHook updates an existing hook with new info (events/url/secret)
  1341  // https://developer.github.com/v3/repos/hooks/#edit-a-hook
  1342  func (c *client) EditRepoHook(org, repo string, id int, req HookRequest) error {
  1343  	c.log("EditRepoHook", org, repo, id)
  1344  	return c.editHook(org, &repo, id, req)
  1345  }
  1346  
  1347  // EditOrgHook updates an existing hook with new info (events/url/secret)
  1348  // https://developer.github.com/v3/orgs/hooks/#edit-a-hook
  1349  func (c *client) EditOrgHook(org string, id int, req HookRequest) error {
  1350  	c.log("EditOrgHook", org, id)
  1351  	return c.editHook(org, nil, id, req)
  1352  }
  1353  
  1354  func (c *client) createHook(org string, repo *string, req HookRequest) (int, error) {
  1355  	if c.dry {
  1356  		return -1, nil
  1357  	}
  1358  	var path string
  1359  	if repo != nil {
  1360  		path = fmt.Sprintf("/repos/%s/%s/hooks", org, *repo)
  1361  	} else {
  1362  		path = fmt.Sprintf("/orgs/%s/hooks", org)
  1363  	}
  1364  	var ret Hook
  1365  	_, err := c.request(&request{
  1366  		method:      http.MethodPost,
  1367  		path:        path,
  1368  		org:         org,
  1369  		exitCodes:   []int{201},
  1370  		requestBody: &req,
  1371  	}, &ret)
  1372  	if err != nil {
  1373  		return 0, err
  1374  	}
  1375  	return ret.ID, nil
  1376  }
  1377  
  1378  // CreateOrgHook creates a new hook for the org
  1379  // https://developer.github.com/v3/orgs/hooks/#create-a-hook
  1380  func (c *client) CreateOrgHook(org string, req HookRequest) (int, error) {
  1381  	c.log("CreateOrgHook", org)
  1382  	return c.createHook(org, nil, req)
  1383  }
  1384  
  1385  // CreateRepoHook creates a new hook for the repo
  1386  // https://developer.github.com/v3/repos/hooks/#create-a-hook
  1387  func (c *client) CreateRepoHook(org, repo string, req HookRequest) (int, error) {
  1388  	c.log("CreateRepoHook", org, repo)
  1389  	return c.createHook(org, &repo, req)
  1390  }
  1391  
  1392  func (c *client) deleteHook(org, path string) error {
  1393  	if c.dry {
  1394  		return nil
  1395  	}
  1396  
  1397  	_, err := c.request(&request{
  1398  		method:    http.MethodDelete,
  1399  		path:      path,
  1400  		org:       org,
  1401  		exitCodes: []int{204},
  1402  	}, nil)
  1403  	return err
  1404  }
  1405  
  1406  // DeleteRepoHook deletes an existing repo level webhook.
  1407  // https://developer.github.com/v3/repos/hooks/#delete-a-hook
  1408  func (c *client) DeleteRepoHook(org, repo string, id int, req HookRequest) error {
  1409  	c.log("DeleteRepoHook", org, repo, id)
  1410  	path := fmt.Sprintf("/repos/%s/%s/hooks/%d", org, repo, id)
  1411  	return c.deleteHook(org, path)
  1412  }
  1413  
  1414  // DeleteOrgHook deletes and existing org level webhook.
  1415  // https://developer.github.com/v3/orgs/hooks/#edit-a-hook
  1416  func (c *client) DeleteOrgHook(org string, id int, req HookRequest) error {
  1417  	c.log("DeleteOrgHook", org, id)
  1418  	path := fmt.Sprintf("/orgs/%s/hooks/%d", org, id)
  1419  	return c.deleteHook(org, path)
  1420  }
  1421  
  1422  // GetOrg returns current metadata for the org
  1423  //
  1424  // https://developer.github.com/v3/orgs/#get-an-organization
  1425  func (c *client) GetOrg(name string) (*Organization, error) {
  1426  	c.log("GetOrg", name)
  1427  	var retOrg Organization
  1428  	_, err := c.request(&request{
  1429  		method:    http.MethodGet,
  1430  		path:      fmt.Sprintf("/orgs/%s", name),
  1431  		org:       name,
  1432  		exitCodes: []int{200},
  1433  	}, &retOrg)
  1434  	if err != nil {
  1435  		return nil, err
  1436  	}
  1437  	return &retOrg, nil
  1438  }
  1439  
  1440  // EditOrg will update the metadata for this org.
  1441  //
  1442  // https://developer.github.com/v3/orgs/#edit-an-organization
  1443  func (c *client) EditOrg(name string, config Organization) (*Organization, error) {
  1444  	c.log("EditOrg", name, config)
  1445  	if c.dry {
  1446  		return &config, nil
  1447  	}
  1448  	var retOrg Organization
  1449  	_, err := c.request(&request{
  1450  		method:      http.MethodPatch,
  1451  		path:        fmt.Sprintf("/orgs/%s", name),
  1452  		org:         name,
  1453  		exitCodes:   []int{200},
  1454  		requestBody: &config,
  1455  	}, &retOrg)
  1456  	if err != nil {
  1457  		return nil, err
  1458  	}
  1459  	return &retOrg, nil
  1460  }
  1461  
  1462  // ListOrgInvitations lists pending invitations to th org.
  1463  //
  1464  // https://developer.github.com/v3/orgs/members/#list-pending-organization-invitations
  1465  func (c *client) ListOrgInvitations(org string) ([]OrgInvitation, error) {
  1466  	c.log("ListOrgInvitations", org)
  1467  	if c.fake {
  1468  		return nil, nil
  1469  	}
  1470  	path := fmt.Sprintf("/orgs/%s/invitations", org)
  1471  	var ret []OrgInvitation
  1472  	err := c.readPaginatedResults(
  1473  		path,
  1474  		acceptNone,
  1475  		org,
  1476  		func() interface{} {
  1477  			return &[]OrgInvitation{}
  1478  		},
  1479  		func(obj interface{}) {
  1480  			ret = append(ret, *(obj.(*[]OrgInvitation))...)
  1481  		},
  1482  	)
  1483  	if err != nil {
  1484  		return nil, err
  1485  	}
  1486  	return ret, nil
  1487  }
  1488  
  1489  // ListCurrentUserRepoInvitations lists pending invitations for the authenticated user.
  1490  //
  1491  // https://docs.github.com/en/rest/reference/repos#list-repository-invitations-for-the-authenticated-user
  1492  func (c *client) ListCurrentUserRepoInvitations() ([]UserRepoInvitation, error) {
  1493  	c.log("ListCurrentUserRepoInvitations")
  1494  	if c.fake {
  1495  		return nil, nil
  1496  	}
  1497  	path := "/user/repository_invitations"
  1498  	var ret []UserRepoInvitation
  1499  	err := c.readPaginatedResults(
  1500  		path,
  1501  		acceptNone,
  1502  		"",
  1503  		func() interface{} {
  1504  			return &[]UserRepoInvitation{}
  1505  		},
  1506  		func(obj interface{}) {
  1507  			ret = append(ret, *(obj.(*[]UserRepoInvitation))...)
  1508  		},
  1509  	)
  1510  	if err != nil {
  1511  		return nil, err
  1512  	}
  1513  	return ret, nil
  1514  }
  1515  
  1516  // AcceptUserRepoInvitation accepts invitation for the authenticated user.
  1517  //
  1518  // https://docs.github.com/en/rest/reference/repos#accept-a-repository-invitation
  1519  func (c *client) AcceptUserRepoInvitation(invitationID int) error {
  1520  	c.log("AcceptUserRepoInvitation", invitationID)
  1521  
  1522  	_, err := c.request(&request{
  1523  		method:    http.MethodPatch,
  1524  		path:      fmt.Sprintf("/user/repository_invitations/%d", invitationID),
  1525  		org:       "",
  1526  		exitCodes: []int{204},
  1527  	}, nil)
  1528  
  1529  	return err
  1530  }
  1531  
  1532  // ListCurrentUserOrgInvitations lists org invitation for the authenticated user.
  1533  //
  1534  // https://docs.github.com/en/rest/reference/orgs#get-organization-membership-for-a-user
  1535  func (c *client) ListCurrentUserOrgInvitations() ([]UserOrgInvitation, error) {
  1536  	c.log("ListCurrentUserOrgInvitations")
  1537  	if c.fake {
  1538  		return nil, nil
  1539  	}
  1540  	path := "/user/memberships/orgs"
  1541  	var ret []UserOrgInvitation
  1542  	err := c.readPaginatedResultsWithValues(
  1543  		path,
  1544  		url.Values{
  1545  			"per_page": []string{"100"},
  1546  			"state":    []string{"pending"},
  1547  		},
  1548  		acceptNone,
  1549  		"",
  1550  		func() interface{} {
  1551  			return &[]UserOrgInvitation{}
  1552  		},
  1553  		func(obj interface{}) {
  1554  			for _, uoi := range *(obj.(*[]UserOrgInvitation)) {
  1555  				if uoi.State == "pending" {
  1556  					ret = append(ret, uoi)
  1557  				}
  1558  			}
  1559  		},
  1560  	)
  1561  	if err != nil {
  1562  		return nil, err
  1563  	}
  1564  	return ret, nil
  1565  }
  1566  
  1567  // AcceptUserOrgInvitation accepts org invitation for the authenticated user.
  1568  //
  1569  // https://docs.github.com/en/rest/reference/orgs#update-an-organization-membership-for-the-authenticated-user
  1570  func (c *client) AcceptUserOrgInvitation(org string) error {
  1571  	c.log("AcceptUserOrgInvitation", org)
  1572  
  1573  	_, err := c.request(&request{
  1574  		method:      http.MethodPatch,
  1575  		path:        fmt.Sprintf("/user/memberships/orgs/%s", org),
  1576  		org:         org,
  1577  		requestBody: map[string]string{"state": "active"},
  1578  		exitCodes:   []int{200},
  1579  	}, nil)
  1580  
  1581  	return err
  1582  }
  1583  
  1584  // ListOrgMembers list all users who are members of an organization. If the authenticated
  1585  // user is also a member of this organization then both concealed and public members
  1586  // will be returned.
  1587  //
  1588  // Role options are "all", "admin" and "member"
  1589  //
  1590  // https://developer.github.com/v3/orgs/members/#members-list
  1591  func (c *client) ListOrgMembers(org, role string) ([]TeamMember, error) {
  1592  	c.log("ListOrgMembers", org, role)
  1593  	if c.fake {
  1594  		return nil, nil
  1595  	}
  1596  	path := fmt.Sprintf("/orgs/%s/members", org)
  1597  	var teamMembers []TeamMember
  1598  	err := c.readPaginatedResultsWithValues(
  1599  		path,
  1600  		url.Values{
  1601  			"per_page": []string{"100"},
  1602  			"role":     []string{role},
  1603  		},
  1604  		acceptNone,
  1605  		org,
  1606  		func() interface{} {
  1607  			return &[]TeamMember{}
  1608  		},
  1609  		func(obj interface{}) {
  1610  			teamMembers = append(teamMembers, *(obj.(*[]TeamMember))...)
  1611  		},
  1612  	)
  1613  	if err != nil {
  1614  		return nil, err
  1615  	}
  1616  	return teamMembers, nil
  1617  }
  1618  
  1619  // HasPermission returns true if GetUserPermission() returns any of the roles.
  1620  func (c *client) HasPermission(org, repo, user string, roles ...string) (bool, error) {
  1621  	perm, err := c.GetUserPermission(org, repo, user)
  1622  	if err != nil {
  1623  		return false, err
  1624  	}
  1625  	for _, r := range roles {
  1626  		if r == perm {
  1627  			return true, nil
  1628  		}
  1629  	}
  1630  	return false, nil
  1631  }
  1632  
  1633  // GetUserPermission returns the user's permission level for a repo
  1634  //
  1635  // https://developer.github.com/v3/repos/collaborators/#review-a-users-permission-level
  1636  func (c *client) GetUserPermission(org, repo, user string) (string, error) {
  1637  	c.log("GetUserPermission", org, repo, user)
  1638  
  1639  	var perm struct {
  1640  		Perm string `json:"permission"`
  1641  	}
  1642  	_, err := c.request(&request{
  1643  		method:    http.MethodGet,
  1644  		path:      fmt.Sprintf("/repos/%s/%s/collaborators/%s/permission", org, repo, user),
  1645  		org:       org,
  1646  		exitCodes: []int{200},
  1647  	}, &perm)
  1648  	if err != nil {
  1649  		return "", err
  1650  	}
  1651  	return perm.Perm, nil
  1652  }
  1653  
  1654  // UpdateOrgMembership invites a user to the org and/or updates their permission level.
  1655  //
  1656  // If the user is not already a member, this will invite them.
  1657  // This will also change the role to/from admin, on either the invitation or membership setting.
  1658  //
  1659  // https://developer.github.com/v3/orgs/members/#add-or-update-organization-membership
  1660  func (c *client) UpdateOrgMembership(org, user string, admin bool) (*OrgMembership, error) {
  1661  	c.log("UpdateOrgMembership", org, user, admin)
  1662  	om := OrgMembership{}
  1663  	if admin {
  1664  		om.Role = RoleAdmin
  1665  	} else {
  1666  		om.Role = RoleMember
  1667  	}
  1668  	if c.dry {
  1669  		return &om, nil
  1670  	}
  1671  
  1672  	_, err := c.request(&request{
  1673  		method:      http.MethodPut,
  1674  		path:        fmt.Sprintf("/orgs/%s/memberships/%s", org, user),
  1675  		org:         org,
  1676  		requestBody: &om,
  1677  		exitCodes:   []int{200},
  1678  	}, &om)
  1679  	return &om, err
  1680  }
  1681  
  1682  // RemoveOrgMembership removes the user from the org.
  1683  //
  1684  // https://developer.github.com/v3/orgs/members/#remove-organization-membership
  1685  func (c *client) RemoveOrgMembership(org, user string) error {
  1686  	c.log("RemoveOrgMembership", org, user)
  1687  	_, err := c.request(&request{
  1688  		method:    http.MethodDelete,
  1689  		org:       org,
  1690  		path:      fmt.Sprintf("/orgs/%s/memberships/%s", org, user),
  1691  		exitCodes: []int{204},
  1692  	}, nil)
  1693  	return err
  1694  }
  1695  
  1696  // CreateComment creates a comment on the issue.
  1697  //
  1698  // See https://developer.github.com/v3/issues/comments/#create-a-comment
  1699  func (c *client) CreateComment(org, repo string, number int, comment string) error {
  1700  	return c.CreateCommentWithContext(context.Background(), org, repo, number, comment)
  1701  }
  1702  
  1703  func (c *client) CreateCommentWithContext(ctx context.Context, org, repo string, number int, comment string) error {
  1704  	c.log("CreateComment", org, repo, number, comment)
  1705  	ic := IssueComment{
  1706  		Body: comment,
  1707  	}
  1708  	_, err := c.requestWithContext(ctx, &request{
  1709  		method:      http.MethodPost,
  1710  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d/comments", org, repo, number),
  1711  		org:         org,
  1712  		requestBody: &ic,
  1713  		exitCodes:   []int{201},
  1714  	}, nil)
  1715  	return err
  1716  }
  1717  
  1718  // DeleteComment deletes the comment.
  1719  //
  1720  // See https://developer.github.com/v3/issues/comments/#delete-a-comment
  1721  func (c *client) DeleteComment(org, repo string, id int) error {
  1722  	return c.DeleteCommentWithContext(context.Background(), org, repo, id)
  1723  }
  1724  
  1725  func (c *client) DeleteCommentWithContext(ctx context.Context, org, repo string, id int) error {
  1726  	c.log("DeleteComment", org, repo, id)
  1727  	_, err := c.requestWithContext(ctx, &request{
  1728  		method:    http.MethodDelete,
  1729  		path:      fmt.Sprintf("/repos/%s/%s/issues/comments/%d", org, repo, id),
  1730  		org:       org,
  1731  		exitCodes: []int{204, 404},
  1732  	}, nil)
  1733  	return err
  1734  }
  1735  
  1736  // EditComment changes the body of comment id in org/repo.
  1737  //
  1738  // See https://developer.github.com/v3/issues/comments/#edit-a-comment
  1739  func (c *client) EditComment(org, repo string, id int, comment string) error {
  1740  	return c.EditCommentWithContext(context.Background(), org, repo, id, comment)
  1741  }
  1742  
  1743  func (c *client) EditCommentWithContext(ctx context.Context, org, repo string, id int, comment string) error {
  1744  	c.log("EditComment", org, repo, id, comment)
  1745  	ic := IssueComment{
  1746  		Body: comment,
  1747  	}
  1748  	_, err := c.requestWithContext(ctx, &request{
  1749  		method:      http.MethodPatch,
  1750  		path:        fmt.Sprintf("/repos/%s/%s/issues/comments/%d", org, repo, id),
  1751  		org:         org,
  1752  		requestBody: &ic,
  1753  		exitCodes:   []int{200},
  1754  	}, nil)
  1755  	return err
  1756  }
  1757  
  1758  // CreateCommentReaction responds emotionally to comment id in org/repo.
  1759  //
  1760  // See https://developer.github.com/v3/reactions/#create-reaction-for-an-issue-comment
  1761  func (c *client) CreateCommentReaction(org, repo string, id int, reaction string) error {
  1762  	c.log("CreateCommentReaction", org, repo, id, reaction)
  1763  	r := Reaction{Content: reaction}
  1764  	_, err := c.request(&request{
  1765  		method:      http.MethodPost,
  1766  		path:        fmt.Sprintf("/repos/%s/%s/issues/comments/%d/reactions", org, repo, id),
  1767  		accept:      "application/vnd.github.squirrel-girl-preview",
  1768  		org:         org,
  1769  		exitCodes:   []int{201},
  1770  		requestBody: &r,
  1771  	}, nil)
  1772  	return err
  1773  }
  1774  
  1775  // CreateIssue creates a new issue and returns its number if
  1776  // the creation is successful, otherwise any error that is encountered.
  1777  //
  1778  // See https://developer.github.com/v3/issues/#create-an-issue
  1779  func (c *client) CreateIssue(org, repo, title, body string, milestone int, labels, assignees []string) (int, error) {
  1780  	durationLogger := c.log("CreateIssue", org, repo, title)
  1781  	defer durationLogger()
  1782  
  1783  	data := struct {
  1784  		Title     string   `json:"title,omitempty"`
  1785  		Body      string   `json:"body,omitempty"`
  1786  		Milestone int      `json:"milestone,omitempty"`
  1787  		Labels    []string `json:"labels,omitempty"`
  1788  		Assignees []string `json:"assignees,omitempty"`
  1789  	}{
  1790  		Title:     title,
  1791  		Body:      body,
  1792  		Milestone: milestone,
  1793  		Labels:    labels,
  1794  		Assignees: assignees,
  1795  	}
  1796  	var resp struct {
  1797  		Num int `json:"number"`
  1798  	}
  1799  	_, err := c.request(&request{
  1800  		// allow the description and draft fields
  1801  		// https://developer.github.com/changes/2019-02-14-draft-pull-requests/
  1802  		accept:      "application/vnd.github+json, application/vnd.github.shadow-cat-preview",
  1803  		method:      http.MethodPost,
  1804  		path:        fmt.Sprintf("/repos/%s/%s/issues", org, repo),
  1805  		org:         org,
  1806  		requestBody: &data,
  1807  		exitCodes:   []int{201},
  1808  	}, &resp)
  1809  	if err != nil {
  1810  		return 0, err
  1811  	}
  1812  	return resp.Num, nil
  1813  }
  1814  
  1815  // CreateIssueReaction responds emotionally to org/repo#id
  1816  //
  1817  // See https://developer.github.com/v3/reactions/#create-reaction-for-an-issue
  1818  func (c *client) CreateIssueReaction(org, repo string, id int, reaction string) error {
  1819  	c.log("CreateIssueReaction", org, repo, id, reaction)
  1820  	r := Reaction{Content: reaction}
  1821  	_, err := c.request(&request{
  1822  		method:      http.MethodPost,
  1823  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d/reactions", org, repo, id),
  1824  		accept:      "application/vnd.github.squirrel-girl-preview",
  1825  		org:         org,
  1826  		requestBody: &r,
  1827  		exitCodes:   []int{200, 201},
  1828  	}, nil)
  1829  	return err
  1830  }
  1831  
  1832  // DeleteStaleComments iterates over comments on an issue/PR, deleting those which the 'isStale'
  1833  // function identifies as stale. If 'comments' is nil, the comments will be fetched from GitHub.
  1834  func (c *client) DeleteStaleComments(org, repo string, number int, comments []IssueComment, isStale func(IssueComment) bool) error {
  1835  	return c.DeleteStaleCommentsWithContext(context.Background(), org, repo, number, comments, isStale)
  1836  }
  1837  
  1838  func (c *client) DeleteStaleCommentsWithContext(ctx context.Context, org, repo string, number int, comments []IssueComment, isStale func(IssueComment) bool) error {
  1839  	var err error
  1840  	if comments == nil {
  1841  		comments, err = c.ListIssueCommentsWithContext(ctx, org, repo, number)
  1842  		if err != nil {
  1843  			return fmt.Errorf("failed to list comments while deleting stale comments. err: %w", err)
  1844  		}
  1845  	}
  1846  	for _, comment := range comments {
  1847  		if isStale(comment) {
  1848  			if err := c.DeleteComment(org, repo, comment.ID); err != nil {
  1849  				return fmt.Errorf("failed to delete stale comment with ID '%d'", comment.ID)
  1850  			}
  1851  		}
  1852  	}
  1853  	return nil
  1854  }
  1855  
  1856  // readPaginatedResults iterates over all objects in the paginated result indicated by the given url.
  1857  //
  1858  // newObj() should return a new slice of the expected type
  1859  // accumulate() should accept that populated slice for each page of results.
  1860  //
  1861  // Returns an error any call to GitHub or object marshalling fails.
  1862  func (c *client) readPaginatedResults(path, accept, org string, newObj func() interface{}, accumulate func(interface{})) error {
  1863  	return c.readPaginatedResultsWithContext(context.Background(), path, accept, org, newObj, accumulate)
  1864  }
  1865  
  1866  func (c *client) readPaginatedResultsWithContext(ctx context.Context, path, accept, org string, newObj func() interface{}, accumulate func(interface{})) error {
  1867  	values := url.Values{
  1868  		"per_page": []string{"100"},
  1869  	}
  1870  	return c.readPaginatedResultsWithValuesWithContext(ctx, path, values, accept, org, newObj, accumulate)
  1871  }
  1872  
  1873  // readPaginatedResultsWithValues is an override that allows control over the query string.
  1874  func (c *client) readPaginatedResultsWithValues(path string, values url.Values, accept, org string, newObj func() interface{}, accumulate func(interface{})) error {
  1875  	return c.readPaginatedResultsWithValuesWithContext(context.Background(), path, values, accept, org, newObj, accumulate)
  1876  }
  1877  
  1878  func (c *client) readPaginatedResultsWithValuesWithContext(ctx context.Context, path string, values url.Values, accept, org string, newObj func() interface{}, accumulate func(interface{})) error {
  1879  	pagedPath := path
  1880  	if len(values) > 0 {
  1881  		pagedPath += "?" + values.Encode()
  1882  	}
  1883  	for {
  1884  		resp, err := c.requestRetryWithContext(ctx, http.MethodGet, pagedPath, accept, org, nil)
  1885  		if err != nil {
  1886  			return err
  1887  		}
  1888  		defer resp.Body.Close()
  1889  		if resp.StatusCode < 200 || resp.StatusCode > 299 {
  1890  			return fmt.Errorf("return code not 2XX: %s", resp.Status)
  1891  		}
  1892  
  1893  		b, err := io.ReadAll(resp.Body)
  1894  		if err != nil {
  1895  			return err
  1896  		}
  1897  
  1898  		obj := newObj()
  1899  		if err := json.Unmarshal(b, obj); err != nil {
  1900  			return err
  1901  		}
  1902  
  1903  		accumulate(obj)
  1904  
  1905  		link := parseLinks(resp.Header.Get("Link"))["next"]
  1906  		if link == "" {
  1907  			break
  1908  		}
  1909  
  1910  		// Example for github.com:
  1911  		// * c.bases[0]: api.github.com
  1912  		// * initial call: api.github.com/repos/kubernetes/kubernetes/pulls?per_page=100
  1913  		// * next: api.github.com/repositories/22/pulls?per_page=100&page=2
  1914  		// * in this case prefix will be empty and we're just calling the path returned by next
  1915  		// Example for github enterprise:
  1916  		// * c.bases[0]: <ghe-url>/api/v3
  1917  		// * initial call: <ghe-url>/api/v3/repos/kubernetes/kubernetes/pulls?per_page=100
  1918  		// * next: <ghe-url>/api/v3/repositories/22/pulls?per_page=100&page=2
  1919  		// * in this case prefix will be "/api/v3" and we will strip the prefix. If we don't do that,
  1920  		//   the next call will go to <ghe-url>/api/v3/api/v3/repositories/22/pulls?per_page=100&page=2
  1921  		prefix := strings.TrimSuffix(resp.Request.URL.RequestURI(), pagedPath)
  1922  
  1923  		u, err := url.Parse(link)
  1924  		if err != nil {
  1925  			return fmt.Errorf("failed to parse 'next' link: %w", err)
  1926  		}
  1927  		pagedPath = strings.TrimPrefix(u.RequestURI(), prefix)
  1928  	}
  1929  	return nil
  1930  }
  1931  
  1932  // ListIssueComments returns all comments on an issue.
  1933  //
  1934  // Each page of results consumes one API token.
  1935  //
  1936  // See https://developer.github.com/v3/issues/comments/#list-comments-on-an-issue
  1937  func (c *client) ListIssueComments(org, repo string, number int) ([]IssueComment, error) {
  1938  	return c.ListIssueCommentsWithContext(context.Background(), org, repo, number)
  1939  }
  1940  
  1941  func (c *client) ListIssueCommentsWithContext(ctx context.Context, org, repo string, number int) ([]IssueComment, error) {
  1942  	c.log("ListIssueComments", org, repo, number)
  1943  	if c.fake {
  1944  		return nil, nil
  1945  	}
  1946  	path := fmt.Sprintf("/repos/%s/%s/issues/%d/comments", org, repo, number)
  1947  	var comments []IssueComment
  1948  	err := c.readPaginatedResultsWithContext(
  1949  		ctx,
  1950  		path,
  1951  		acceptNone,
  1952  		org,
  1953  		func() interface{} {
  1954  			return &[]IssueComment{}
  1955  		},
  1956  		func(obj interface{}) {
  1957  			comments = append(comments, *(obj.(*[]IssueComment))...)
  1958  		},
  1959  	)
  1960  	if err != nil {
  1961  		return nil, err
  1962  	}
  1963  	return comments, nil
  1964  }
  1965  
  1966  // ListOpenIssues returns all open issues, including pull requests
  1967  //
  1968  // Each page of results consumes one API token.
  1969  //
  1970  // See https://developer.github.com/v3/issues/#list-issues-for-a-repository
  1971  func (c *client) ListOpenIssues(org, repo string) ([]Issue, error) {
  1972  	c.log("ListOpenIssues", org, repo)
  1973  	if c.fake {
  1974  		return nil, nil
  1975  	}
  1976  	path := fmt.Sprintf("/repos/%s/%s/issues", org, repo)
  1977  	var issues []Issue
  1978  	err := c.readPaginatedResults(
  1979  		path,
  1980  		acceptNone,
  1981  		org,
  1982  		func() interface{} {
  1983  			return &[]Issue{}
  1984  		},
  1985  		func(obj interface{}) {
  1986  			issues = append(issues, *(obj.(*[]Issue))...)
  1987  		},
  1988  	)
  1989  	if err != nil {
  1990  		return nil, err
  1991  	}
  1992  	return issues, nil
  1993  }
  1994  
  1995  // GetPullRequests get all open pull requests for a repo.
  1996  //
  1997  // See https://developer.github.com/v3/pulls/#list-pull-requests
  1998  func (c *client) GetPullRequests(org, repo string) ([]PullRequest, error) {
  1999  	c.log("GetPullRequests", org, repo)
  2000  	var prs []PullRequest
  2001  	if c.fake {
  2002  		return prs, nil
  2003  	}
  2004  	path := fmt.Sprintf("/repos/%s/%s/pulls", org, repo)
  2005  	err := c.readPaginatedResults(
  2006  		path,
  2007  		// allow the description and draft fields
  2008  		// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2009  		// https://developer.github.com/changes/2019-02-14-draft-pull-requests/
  2010  		"application/vnd.github.symmetra-preview+json, application/vnd.github.shadow-cat-preview",
  2011  		org,
  2012  		func() interface{} {
  2013  			return &[]PullRequest{}
  2014  		},
  2015  		func(obj interface{}) {
  2016  			prs = append(prs, *(obj.(*[]PullRequest))...)
  2017  		},
  2018  	)
  2019  	if err != nil {
  2020  		return nil, err
  2021  	}
  2022  	return prs, err
  2023  }
  2024  
  2025  // GetPullRequest gets a pull request.
  2026  //
  2027  // See https://developer.github.com/v3/pulls/#get-a-single-pull-request
  2028  func (c *client) GetPullRequest(org, repo string, number int) (*PullRequest, error) {
  2029  	durationLogger := c.log("GetPullRequest", org, repo, number)
  2030  	defer durationLogger()
  2031  
  2032  	var pr PullRequest
  2033  	_, err := c.request(&request{
  2034  		// allow the description and draft fields
  2035  		// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2036  		// https://developer.github.com/changes/2019-02-14-draft-pull-requests/
  2037  		accept:    "application/vnd.github.symmetra-preview+json, application/vnd.github.shadow-cat-preview",
  2038  		method:    http.MethodGet,
  2039  		path:      fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  2040  		org:       org,
  2041  		exitCodes: []int{200},
  2042  	}, &pr)
  2043  	return &pr, err
  2044  }
  2045  
  2046  func (c *client) GetFailedActionRunsByHeadBranch(org, repo, branchName, headSHA string) ([]WorkflowRun, error) {
  2047  	durationLogger := c.log("GetJobsByHeadBranch", org, repo)
  2048  	defer durationLogger()
  2049  
  2050  	var runs WorkflowRuns
  2051  
  2052  	u := url.URL{
  2053  		Path: fmt.Sprintf("/repos/%s/%s/actions/runs", org, repo),
  2054  	}
  2055  	query := u.Query()
  2056  	query.Add("status", "failure")
  2057  	// setting the OR condition to get both PR and PR target workflows
  2058  	query.Add("event", "pull_request OR pull_request_target")
  2059  	query.Add("branch", branchName)
  2060  	u.RawQuery = query.Encode()
  2061  
  2062  	_, err := c.request(&request{
  2063  		accept:    "application/vnd.github.v3+json",
  2064  		method:    http.MethodGet,
  2065  		path:      u.String(),
  2066  		org:       org,
  2067  		exitCodes: []int{200},
  2068  	}, &runs)
  2069  
  2070  	prRuns := []WorkflowRun{}
  2071  
  2072  	// keep only the runs matching the current PR headSHA
  2073  	for _, run := range runs.WorflowRuns {
  2074  		if run.HeadSha == headSHA {
  2075  			prRuns = append(prRuns, run)
  2076  		}
  2077  	}
  2078  
  2079  	return prRuns, err
  2080  }
  2081  
  2082  // TriggerGitHubWorkflow will rerun a workflow
  2083  //
  2084  // See https://docs.github.com/en/rest/actions/workflow-runs#re-run-a-workflow
  2085  func (c *client) TriggerGitHubWorkflow(org, repo string, id int) error {
  2086  	durationLogger := c.log("TriggerGitHubWorkflow", org, repo, id)
  2087  	defer durationLogger()
  2088  	_, err := c.request(&request{
  2089  		accept:    "application/vnd.github.v3+json",
  2090  		method:    http.MethodPost,
  2091  		path:      fmt.Sprintf("/repos/%s/%s/actions/runs/%d/rerun", org, repo, id),
  2092  		org:       org,
  2093  		exitCodes: []int{201},
  2094  	}, nil)
  2095  	return err
  2096  }
  2097  
  2098  // TriggerFailedGitHubWorkflow will rerun the failed jobs and all its dependents
  2099  //
  2100  // See https://docs.github.com/en/rest/actions/workflow-runs#re-run-failed-jobs-from-a-workflow-run
  2101  func (c *client) TriggerFailedGitHubWorkflow(org, repo string, id int) error {
  2102  	durationLogger := c.log("TriggerFailedGitHubWorkflow", org, repo, id)
  2103  	defer durationLogger()
  2104  	_, err := c.request(&request{
  2105  		accept:    "application/vnd.github.v3+json",
  2106  		method:    http.MethodPost,
  2107  		path:      fmt.Sprintf("/repos/%s/%s/actions/runs/%d/rerun-failed-jobs", org, repo, id),
  2108  		org:       org,
  2109  		exitCodes: []int{201},
  2110  	}, nil)
  2111  	return err
  2112  }
  2113  
  2114  // EditPullRequest will update the pull request.
  2115  //
  2116  // See https://developer.github.com/v3/pulls/#update-a-pull-request
  2117  func (c *client) EditPullRequest(org, repo string, number int, pr *PullRequest) (*PullRequest, error) {
  2118  	durationLogger := c.log("EditPullRequest", org, repo, number)
  2119  	defer durationLogger()
  2120  
  2121  	if c.dry {
  2122  		return pr, nil
  2123  	}
  2124  	edit := struct {
  2125  		Title string `json:"title,omitempty"`
  2126  		Body  string `json:"body,omitempty"`
  2127  		State string `json:"state,omitempty"`
  2128  	}{
  2129  		Title: pr.Title,
  2130  		Body:  pr.Body,
  2131  		State: pr.State,
  2132  	}
  2133  	var ret PullRequest
  2134  	_, err := c.request(&request{
  2135  		method:      http.MethodPatch,
  2136  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  2137  		org:         org,
  2138  		exitCodes:   []int{200},
  2139  		requestBody: &edit,
  2140  	}, &ret)
  2141  	if err != nil {
  2142  		return nil, err
  2143  	}
  2144  	return &ret, nil
  2145  }
  2146  
  2147  // GetIssue gets an issue.
  2148  //
  2149  // See https://developer.github.com/v3/issues/#get-a-single-issue
  2150  func (c *client) GetIssue(org, repo string, number int) (*Issue, error) {
  2151  	durationLogger := c.log("GetIssue", org, repo, number)
  2152  	defer durationLogger()
  2153  
  2154  	var i Issue
  2155  	_, err := c.request(&request{
  2156  		// allow emoji
  2157  		// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2158  		accept:    "application/vnd.github.symmetra-preview+json",
  2159  		method:    http.MethodGet,
  2160  		path:      fmt.Sprintf("/repos/%s/%s/issues/%d", org, repo, number),
  2161  		org:       org,
  2162  		exitCodes: []int{200},
  2163  	}, &i)
  2164  	return &i, err
  2165  }
  2166  
  2167  // EditIssue will update the issue.
  2168  //
  2169  // See https://developer.github.com/v3/issues/#edit-an-issue
  2170  func (c *client) EditIssue(org, repo string, number int, issue *Issue) (*Issue, error) {
  2171  	durationLogger := c.log("EditIssue", org, repo, number)
  2172  	defer durationLogger()
  2173  
  2174  	if c.dry {
  2175  		return issue, nil
  2176  	}
  2177  	edit := struct {
  2178  		Title string `json:"title,omitempty"`
  2179  		Body  string `json:"body,omitempty"`
  2180  		State string `json:"state,omitempty"`
  2181  	}{
  2182  		Title: issue.Title,
  2183  		Body:  issue.Body,
  2184  		State: issue.State,
  2185  	}
  2186  	var ret Issue
  2187  	_, err := c.request(&request{
  2188  		method:      http.MethodPatch,
  2189  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d", org, repo, number),
  2190  		org:         org,
  2191  		exitCodes:   []int{200},
  2192  		requestBody: &edit,
  2193  	}, &ret)
  2194  	if err != nil {
  2195  		return nil, err
  2196  	}
  2197  	return &ret, nil
  2198  }
  2199  
  2200  // GetPullRequestDiff gets the diff version of a pull request.
  2201  //
  2202  // See https://docs.github.com/en/rest/overview/media-types?apiVersion=2022-11-28#commits-commit-comparison-and-pull-requests
  2203  func (c *client) GetPullRequestDiff(org, repo string, number int) ([]byte, error) {
  2204  	durationLogger := c.log("GetPullRequestDiff", org, repo, number)
  2205  	defer durationLogger()
  2206  
  2207  	_, diff, err := c.requestRaw(&request{
  2208  		accept:    "application/vnd.github.diff",
  2209  		method:    http.MethodGet,
  2210  		path:      fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  2211  		org:       org,
  2212  		exitCodes: []int{200},
  2213  	})
  2214  	return diff, err
  2215  }
  2216  
  2217  // GetPullRequestPatch gets the patch version of a pull request.
  2218  //
  2219  // See https://docs.github.com/en/rest/overview/media-types?apiVersion=2022-11-28#commits-commit-comparison-and-pull-requests
  2220  func (c *client) GetPullRequestPatch(org, repo string, number int) ([]byte, error) {
  2221  	durationLogger := c.log("GetPullRequestPatch", org, repo, number)
  2222  	defer durationLogger()
  2223  
  2224  	_, patch, err := c.requestRaw(&request{
  2225  		accept:    "application/vnd.github.patch",
  2226  		method:    http.MethodGet,
  2227  		path:      fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  2228  		org:       org,
  2229  		exitCodes: []int{200},
  2230  	})
  2231  	return patch, err
  2232  }
  2233  
  2234  // CreatePullRequest creates a new pull request and returns its number if
  2235  // the creation is successful, otherwise any error that is encountered.
  2236  //
  2237  // See https://developer.github.com/v3/pulls/#create-a-pull-request
  2238  func (c *client) CreatePullRequest(org, repo, title, body, head, base string, canModify bool) (int, error) {
  2239  	durationLogger := c.log("CreatePullRequest", org, repo, title)
  2240  	defer durationLogger()
  2241  
  2242  	data := struct {
  2243  		Title string `json:"title"`
  2244  		Body  string `json:"body"`
  2245  		Head  string `json:"head"`
  2246  		Base  string `json:"base"`
  2247  		// MaintainerCanModify allows maintainers of the repo to modify this
  2248  		// pull request, eg. push changes to it before merging.
  2249  		MaintainerCanModify bool `json:"maintainer_can_modify"`
  2250  	}{
  2251  		Title: title,
  2252  		Body:  body,
  2253  		Head:  head,
  2254  		Base:  base,
  2255  
  2256  		MaintainerCanModify: canModify,
  2257  	}
  2258  	var resp struct {
  2259  		Num int `json:"number"`
  2260  	}
  2261  	_, err := c.request(&request{
  2262  		// allow the description and draft fields
  2263  		// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2264  		// https://developer.github.com/changes/2019-02-14-draft-pull-requests/
  2265  		accept:      "application/vnd.github.symmetra-preview+json, application/vnd.github.shadow-cat-preview",
  2266  		method:      http.MethodPost,
  2267  		path:        fmt.Sprintf("/repos/%s/%s/pulls", org, repo),
  2268  		org:         org,
  2269  		requestBody: &data,
  2270  		exitCodes:   []int{201},
  2271  	}, &resp)
  2272  	if err != nil {
  2273  		return 0, fmt.Errorf("failed to create pull request against %s/%s#%s from head %s: %w", org, repo, base, head, err)
  2274  	}
  2275  	return resp.Num, nil
  2276  }
  2277  
  2278  // UpdatePullRequest modifies the title, body, open state
  2279  func (c *client) UpdatePullRequest(org, repo string, number int, title, body *string, open *bool, branch *string, canModify *bool) error {
  2280  	durationLogger := c.log("UpdatePullRequest", org, repo, title)
  2281  	defer durationLogger()
  2282  
  2283  	data := struct {
  2284  		State *string `json:"state,omitempty"`
  2285  		Title *string `json:"title,omitempty"`
  2286  		Body  *string `json:"body,omitempty"`
  2287  		Base  *string `json:"base,omitempty"`
  2288  		// MaintainerCanModify allows maintainers of the repo to modify this
  2289  		// pull request, eg. push changes to it before merging.
  2290  		MaintainerCanModify *bool `json:"maintainer_can_modify,omitempty"`
  2291  	}{
  2292  		Title:               title,
  2293  		Body:                body,
  2294  		Base:                branch,
  2295  		MaintainerCanModify: canModify,
  2296  	}
  2297  	if open != nil && *open {
  2298  		op := "open"
  2299  		data.State = &op
  2300  	} else if open != nil {
  2301  		cl := "closed"
  2302  		data.State = &cl
  2303  	}
  2304  	_, err := c.request(&request{
  2305  		// allow the description and draft fields
  2306  		// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2307  		// https://developer.github.com/changes/2019-02-14-draft-pull-requests/
  2308  		accept:      "application/vnd.github.symmetra-preview+json, application/vnd.github.shadow-cat-preview",
  2309  		method:      http.MethodPatch,
  2310  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  2311  		org:         org,
  2312  		requestBody: &data,
  2313  		exitCodes:   []int{200},
  2314  	}, nil)
  2315  	return err
  2316  }
  2317  
  2318  // GetPullRequestChanges gets a list of files modified in a pull request.
  2319  //
  2320  // See https://developer.github.com/v3/pulls/#list-pull-requests-files
  2321  func (c *client) GetPullRequestChanges(org, repo string, number int) ([]PullRequestChange, error) {
  2322  	durationLogger := c.log("GetPullRequestChanges", org, repo, number)
  2323  	defer durationLogger()
  2324  
  2325  	if c.fake {
  2326  		return []PullRequestChange{}, nil
  2327  	}
  2328  	path := fmt.Sprintf("/repos/%s/%s/pulls/%d/files", org, repo, number)
  2329  	var changes []PullRequestChange
  2330  	err := c.readPaginatedResults(
  2331  		path,
  2332  		acceptNone,
  2333  		org,
  2334  		func() interface{} {
  2335  			return &[]PullRequestChange{}
  2336  		},
  2337  		func(obj interface{}) {
  2338  			changes = append(changes, *(obj.(*[]PullRequestChange))...)
  2339  		},
  2340  	)
  2341  	if err != nil {
  2342  		return nil, err
  2343  	}
  2344  	return changes, nil
  2345  }
  2346  
  2347  // ListPullRequestComments returns all *review* comments on a pull request.
  2348  //
  2349  // Multiple-pages of comments consumes multiple API tokens.
  2350  //
  2351  // See https://developer.github.com/v3/pulls/comments/#list-comments-on-a-pull-request
  2352  func (c *client) ListPullRequestComments(org, repo string, number int) ([]ReviewComment, error) {
  2353  	durationLogger := c.log("ListPullRequestComments", org, repo, number)
  2354  	defer durationLogger()
  2355  
  2356  	if c.fake {
  2357  		return nil, nil
  2358  	}
  2359  	path := fmt.Sprintf("/repos/%s/%s/pulls/%d/comments", org, repo, number)
  2360  	var comments []ReviewComment
  2361  	err := c.readPaginatedResults(
  2362  		path,
  2363  		acceptNone,
  2364  		org,
  2365  		func() interface{} {
  2366  			return &[]ReviewComment{}
  2367  		},
  2368  		func(obj interface{}) {
  2369  			comments = append(comments, *(obj.(*[]ReviewComment))...)
  2370  		},
  2371  	)
  2372  	if err != nil {
  2373  		return nil, err
  2374  	}
  2375  	return comments, nil
  2376  }
  2377  
  2378  // ListReviews returns all reviews on a pull request.
  2379  //
  2380  // Multiple-pages of results consumes multiple API tokens.
  2381  //
  2382  // See https://developer.github.com/v3/pulls/reviews/#list-reviews-on-a-pull-request
  2383  func (c *client) ListReviews(org, repo string, number int) ([]Review, error) {
  2384  	durationLogger := c.log("ListReviews", org, repo, number)
  2385  	defer durationLogger()
  2386  
  2387  	if c.fake {
  2388  		return nil, nil
  2389  	}
  2390  	path := fmt.Sprintf("/repos/%s/%s/pulls/%d/reviews", org, repo, number)
  2391  	var reviews []Review
  2392  	err := c.readPaginatedResults(
  2393  		path,
  2394  		acceptNone,
  2395  		org,
  2396  		func() interface{} {
  2397  			return &[]Review{}
  2398  		},
  2399  		func(obj interface{}) {
  2400  			reviews = append(reviews, *(obj.(*[]Review))...)
  2401  		},
  2402  	)
  2403  	if err != nil {
  2404  		return nil, err
  2405  	}
  2406  	return reviews, nil
  2407  }
  2408  
  2409  // CreateStatus creates or updates the status of a commit.
  2410  //
  2411  // See https://docs.github.com/en/rest/reference/commits#create-a-commit-status
  2412  func (c *client) CreateStatus(org, repo, SHA string, s Status) error {
  2413  	return c.CreateStatusWithContext(context.Background(), org, repo, SHA, s)
  2414  }
  2415  
  2416  func (c *client) CreateStatusWithContext(ctx context.Context, org, repo, SHA string, s Status) error {
  2417  	durationLogger := c.log("CreateStatus", org, repo, SHA, s)
  2418  	defer durationLogger()
  2419  	_, err := c.requestWithContext(ctx, &request{
  2420  		method:      http.MethodPost,
  2421  		path:        fmt.Sprintf("/repos/%s/%s/statuses/%s", org, repo, SHA),
  2422  		org:         org,
  2423  		requestBody: &s,
  2424  		exitCodes:   []int{201},
  2425  	}, nil)
  2426  	return err
  2427  }
  2428  
  2429  // ListStatuses gets commit statuses for a given ref.
  2430  //
  2431  // See https://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
  2432  func (c *client) ListStatuses(org, repo, ref string) ([]Status, error) {
  2433  	durationLogger := c.log("ListStatuses", org, repo, ref)
  2434  	defer durationLogger()
  2435  
  2436  	path := fmt.Sprintf("/repos/%s/%s/statuses/%s", org, repo, ref)
  2437  	var statuses []Status
  2438  	err := c.readPaginatedResults(
  2439  		path,
  2440  		acceptNone,
  2441  		org,
  2442  		func() interface{} {
  2443  			return &[]Status{}
  2444  		},
  2445  		func(obj interface{}) {
  2446  			statuses = append(statuses, *(obj.(*[]Status))...)
  2447  		},
  2448  	)
  2449  	return statuses, err
  2450  }
  2451  
  2452  // GetRepo returns the repo for the provided owner/name combination.
  2453  //
  2454  // See https://developer.github.com/v3/repos/#get
  2455  func (c *client) GetRepo(owner, name string) (FullRepo, error) {
  2456  	durationLogger := c.log("GetRepo", owner, name)
  2457  	defer durationLogger()
  2458  
  2459  	var repo FullRepo
  2460  	_, err := c.request(&request{
  2461  		method:    http.MethodGet,
  2462  		path:      fmt.Sprintf("/repos/%s/%s", owner, name),
  2463  		org:       owner,
  2464  		exitCodes: []int{200},
  2465  	}, &repo)
  2466  	return repo, err
  2467  }
  2468  
  2469  // CreateRepo creates a new repository
  2470  // See https://developer.github.com/v3/repos/#create
  2471  func (c *client) CreateRepo(owner string, isUser bool, repo RepoCreateRequest) (*FullRepo, error) {
  2472  	durationLogger := c.log("CreateRepo", owner, isUser, repo)
  2473  	defer durationLogger()
  2474  
  2475  	if repo.Name == nil || *repo.Name == "" {
  2476  		return nil, errors.New("repo.Name must be non-empty")
  2477  	}
  2478  	if c.fake {
  2479  		return nil, nil
  2480  	} else if c.dry {
  2481  		return repo.ToRepo(), nil
  2482  	}
  2483  
  2484  	path := "/user/repos"
  2485  	if !isUser {
  2486  		path = fmt.Sprintf("/orgs/%s/repos", owner)
  2487  	}
  2488  	var retRepo FullRepo
  2489  	_, err := c.request(&request{
  2490  		method:      http.MethodPost,
  2491  		path:        path,
  2492  		org:         owner,
  2493  		requestBody: &repo,
  2494  		exitCodes:   []int{201},
  2495  	}, &retRepo)
  2496  	return &retRepo, err
  2497  }
  2498  
  2499  // UpdateRepo edits an existing repository
  2500  // See https://developer.github.com/v3/repos/#edit
  2501  func (c *client) UpdateRepo(owner, name string, repo RepoUpdateRequest) (*FullRepo, error) {
  2502  	durationLogger := c.log("UpdateRepo", owner, name, repo)
  2503  	defer durationLogger()
  2504  
  2505  	if c.fake {
  2506  		return nil, nil
  2507  	} else if c.dry {
  2508  		return repo.ToRepo(), nil
  2509  	}
  2510  
  2511  	path := fmt.Sprintf("/repos/%s/%s", owner, name)
  2512  	var retRepo FullRepo
  2513  	_, err := c.request(&request{
  2514  		method:      http.MethodPatch,
  2515  		path:        path,
  2516  		org:         owner,
  2517  		requestBody: &repo,
  2518  		exitCodes:   []int{200},
  2519  	}, &retRepo)
  2520  	return &retRepo, err
  2521  }
  2522  
  2523  // GetRepos returns all repos in an org.
  2524  //
  2525  // This call uses multiple API tokens when results are paginated.
  2526  //
  2527  // See https://developer.github.com/v3/repos/#list-organization-repositories
  2528  func (c *client) GetRepos(org string, isUser bool) ([]Repo, error) {
  2529  	durationLogger := c.log("GetRepos", org, isUser)
  2530  	defer durationLogger()
  2531  
  2532  	var (
  2533  		repos   []Repo
  2534  		nextURL string
  2535  	)
  2536  	if c.fake {
  2537  		return repos, nil
  2538  	}
  2539  	if isUser {
  2540  		nextURL = fmt.Sprintf("/users/%s/repos", org)
  2541  	} else {
  2542  		nextURL = fmt.Sprintf("/orgs/%s/repos", org)
  2543  	}
  2544  	err := c.readPaginatedResults(
  2545  		nextURL,    // path
  2546  		acceptNone, // accept
  2547  		org,
  2548  		func() interface{} { // newObj
  2549  			return &[]Repo{}
  2550  		},
  2551  		func(obj interface{}) { // accumulate
  2552  			repos = append(repos, *(obj.(*[]Repo))...)
  2553  		},
  2554  	)
  2555  	if err != nil {
  2556  		return nil, err
  2557  	}
  2558  	return repos, nil
  2559  }
  2560  
  2561  // GetSingleCommit returns a single commit.
  2562  //
  2563  // See https://developer.github.com/v3/repos/#get
  2564  func (c *client) GetSingleCommit(org, repo, SHA string) (RepositoryCommit, error) {
  2565  	durationLogger := c.log("GetSingleCommit", org, repo, SHA)
  2566  	defer durationLogger()
  2567  
  2568  	var commit RepositoryCommit
  2569  	_, err := c.request(&request{
  2570  		method:    http.MethodGet,
  2571  		path:      fmt.Sprintf("/repos/%s/%s/commits/%s", org, repo, SHA),
  2572  		org:       org,
  2573  		exitCodes: []int{200},
  2574  	}, &commit)
  2575  	return commit, err
  2576  }
  2577  
  2578  // GetBranches returns all branches in the repo.
  2579  //
  2580  // If onlyProtected is true it will only return repos with protection enabled,
  2581  // and branch.Protected will be true. Otherwise Protected is the default value (false).
  2582  //
  2583  // This call uses multiple API tokens when results are paginated.
  2584  //
  2585  // See https://developer.github.com/v3/repos/branches/#list-branches
  2586  func (c *client) GetBranches(org, repo string, onlyProtected bool) ([]Branch, error) {
  2587  	durationLogger := c.log("GetBranches", org, repo, onlyProtected)
  2588  	defer durationLogger()
  2589  
  2590  	var branches []Branch
  2591  	err := c.readPaginatedResultsWithValues(
  2592  		fmt.Sprintf("/repos/%s/%s/branches", org, repo),
  2593  		url.Values{
  2594  			"protected": []string{strconv.FormatBool(onlyProtected)},
  2595  			"per_page":  []string{"100"},
  2596  		},
  2597  		acceptNone,
  2598  		org,
  2599  		func() interface{} { // newObj
  2600  			return &[]Branch{}
  2601  		},
  2602  		func(obj interface{}) {
  2603  			branches = append(branches, *(obj.(*[]Branch))...)
  2604  		},
  2605  	)
  2606  	if err != nil {
  2607  		return nil, err
  2608  	}
  2609  	return branches, nil
  2610  }
  2611  
  2612  // GetBranchProtection returns current protection object for the branch
  2613  //
  2614  // See https://developer.github.com/v3/repos/branches/#get-branch-protection
  2615  func (c *client) GetBranchProtection(org, repo, branch string) (*BranchProtection, error) {
  2616  	durationLogger := c.log("GetBranchProtection", org, repo, branch)
  2617  	defer durationLogger()
  2618  
  2619  	code, body, err := c.requestRaw(&request{
  2620  		method: http.MethodGet,
  2621  		path:   fmt.Sprintf("/repos/%s/%s/branches/%s/protection", org, repo, branch),
  2622  		org:    org,
  2623  		// GitHub returns 404 for this call if either:
  2624  		// - The branch is not protected
  2625  		// - The access token used does not have sufficient privileges
  2626  		// We therefore need to introspect the response body.
  2627  		exitCodes: []int{200, 404},
  2628  	})
  2629  
  2630  	switch {
  2631  	case err != nil:
  2632  		return nil, err
  2633  	case code == 200:
  2634  		var bp BranchProtection
  2635  		if err := json.Unmarshal(body, &bp); err != nil {
  2636  			return nil, err
  2637  		}
  2638  		return &bp, nil
  2639  	case code == 404:
  2640  		// continue
  2641  	default:
  2642  		return nil, fmt.Errorf("unexpected status code: %d", code)
  2643  	}
  2644  
  2645  	var ge githubError
  2646  	if err := json.Unmarshal(body, &ge); err != nil {
  2647  		return nil, err
  2648  	}
  2649  
  2650  	// If the error was because the branch is not protected, we return a
  2651  	// nil pointer to indicate this.
  2652  	if ge.Message == "Branch not protected" {
  2653  		return nil, nil
  2654  	}
  2655  
  2656  	// Otherwise we got some other 404 error.
  2657  	return nil, fmt.Errorf("getting branch protection 404: %s", ge.Message)
  2658  }
  2659  
  2660  // RemoveBranchProtection unprotects org/repo=branch.
  2661  //
  2662  // See https://developer.github.com/v3/repos/branches/#remove-branch-protection
  2663  func (c *client) RemoveBranchProtection(org, repo, branch string) error {
  2664  	durationLogger := c.log("RemoveBranchProtection", org, repo, branch)
  2665  	defer durationLogger()
  2666  
  2667  	_, err := c.request(&request{
  2668  		method:    http.MethodDelete,
  2669  		path:      fmt.Sprintf("/repos/%s/%s/branches/%s/protection", org, repo, branch),
  2670  		org:       org,
  2671  		exitCodes: []int{204},
  2672  	}, nil)
  2673  	return err
  2674  }
  2675  
  2676  // UpdateBranchProtection configures org/repo=branch.
  2677  //
  2678  // See https://developer.github.com/v3/repos/branches/#update-branch-protection
  2679  func (c *client) UpdateBranchProtection(org, repo, branch string, config BranchProtectionRequest) error {
  2680  	durationLogger := c.log("UpdateBranchProtection", org, repo, branch, config)
  2681  	defer durationLogger()
  2682  
  2683  	_, err := c.request(&request{
  2684  		method:      http.MethodPut,
  2685  		path:        fmt.Sprintf("/repos/%s/%s/branches/%s/protection", org, repo, branch),
  2686  		org:         org,
  2687  		requestBody: config,
  2688  		exitCodes:   []int{200},
  2689  	}, nil)
  2690  	return err
  2691  }
  2692  
  2693  // AddRepoLabel adds a defined label given org/repo
  2694  //
  2695  // See https://developer.github.com/v3/issues/labels/#create-a-label
  2696  func (c *client) AddRepoLabel(org, repo, label, description, color string) error {
  2697  	durationLogger := c.log("AddRepoLabel", org, repo, label, description, color)
  2698  	defer durationLogger()
  2699  
  2700  	_, err := c.request(&request{
  2701  		method:      http.MethodPost,
  2702  		path:        fmt.Sprintf("/repos/%s/%s/labels", org, repo),
  2703  		accept:      "application/vnd.github.symmetra-preview+json", // allow the description field -- https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2704  		org:         org,
  2705  		requestBody: Label{Name: label, Description: description, Color: color},
  2706  		exitCodes:   []int{201},
  2707  	}, nil)
  2708  	return err
  2709  }
  2710  
  2711  // UpdateRepoLabel updates a org/repo label to new name, description, and color
  2712  //
  2713  // See https://developer.github.com/v3/issues/labels/#update-a-label
  2714  func (c *client) UpdateRepoLabel(org, repo, label, newName, description, color string) error {
  2715  	durationLogger := c.log("UpdateRepoLabel", org, repo, label, newName, color)
  2716  	defer durationLogger()
  2717  
  2718  	_, err := c.request(&request{
  2719  		method:      http.MethodPatch,
  2720  		path:        fmt.Sprintf("/repos/%s/%s/labels/%s", org, repo, label),
  2721  		accept:      "application/vnd.github.symmetra-preview+json", // allow the description field -- https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2722  		org:         org,
  2723  		requestBody: Label{Name: newName, Description: description, Color: color},
  2724  		exitCodes:   []int{200},
  2725  	}, nil)
  2726  	return err
  2727  }
  2728  
  2729  // DeleteRepoLabel deletes a label in org/repo
  2730  //
  2731  // See https://developer.github.com/v3/issues/labels/#delete-a-label
  2732  func (c *client) DeleteRepoLabel(org, repo, label string) error {
  2733  	durationLogger := c.log("DeleteRepoLabel", org, repo, label)
  2734  	defer durationLogger()
  2735  
  2736  	_, err := c.request(&request{
  2737  		method:      http.MethodDelete,
  2738  		accept:      "application/vnd.github.symmetra-preview+json", // allow the description field -- https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2739  		path:        fmt.Sprintf("/repos/%s/%s/labels/%s", org, repo, label),
  2740  		org:         org,
  2741  		requestBody: Label{Name: label},
  2742  		exitCodes:   []int{204},
  2743  	}, nil)
  2744  	return err
  2745  }
  2746  
  2747  // GetCombinedStatus returns the latest statuses for a given ref.
  2748  //
  2749  // See https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
  2750  func (c *client) GetCombinedStatus(org, repo, ref string) (*CombinedStatus, error) {
  2751  	durationLogger := c.log("GetCombinedStatus", org, repo, ref)
  2752  	defer durationLogger()
  2753  
  2754  	var combinedStatus CombinedStatus
  2755  	err := c.readPaginatedResults(
  2756  		fmt.Sprintf("/repos/%s/%s/commits/%s/status", org, repo, ref),
  2757  		"",
  2758  		org,
  2759  		func() interface{} {
  2760  			return &CombinedStatus{}
  2761  		},
  2762  		func(obj interface{}) {
  2763  			cs := *(obj.(*CombinedStatus))
  2764  			cs.Statuses = append(combinedStatus.Statuses, cs.Statuses...)
  2765  			combinedStatus = cs
  2766  		},
  2767  	)
  2768  	return &combinedStatus, err
  2769  }
  2770  
  2771  // getLabels is a helper function that retrieves a paginated list of labels from a github URI path.
  2772  func (c *client) getLabels(path, org string) ([]Label, error) {
  2773  	var labels []Label
  2774  	if c.fake {
  2775  		return labels, nil
  2776  	}
  2777  	err := c.readPaginatedResults(
  2778  		path,
  2779  		"application/vnd.github.symmetra-preview+json", // allow the description field -- https://developer.github.com/changes/2018-02-22-label-description-search-preview/
  2780  		org,
  2781  		func() interface{} {
  2782  			return &[]Label{}
  2783  		},
  2784  		func(obj interface{}) {
  2785  			labels = append(labels, *(obj.(*[]Label))...)
  2786  		},
  2787  	)
  2788  	if err != nil {
  2789  		return nil, err
  2790  	}
  2791  	return labels, nil
  2792  }
  2793  
  2794  // GetRepoLabels returns the list of labels accessible to org/repo.
  2795  //
  2796  // See https://developer.github.com/v3/issues/labels/#list-all-labels-for-this-repository
  2797  func (c *client) GetRepoLabels(org, repo string) ([]Label, error) {
  2798  	durationLogger := c.log("GetRepoLabels", org, repo)
  2799  	defer durationLogger()
  2800  
  2801  	return c.getLabels(fmt.Sprintf("/repos/%s/%s/labels", org, repo), org)
  2802  }
  2803  
  2804  // GetIssueLabels returns the list of labels currently on issue org/repo#number.
  2805  //
  2806  // See https://developer.github.com/v3/issues/labels/#list-labels-on-an-issue
  2807  func (c *client) GetIssueLabels(org, repo string, number int) ([]Label, error) {
  2808  	durationLogger := c.log("GetIssueLabels", org, repo, number)
  2809  	defer durationLogger()
  2810  
  2811  	return c.getLabels(fmt.Sprintf("/repos/%s/%s/issues/%d/labels", org, repo, number), org)
  2812  }
  2813  
  2814  // AddLabel adds label to org/repo#number, returning an error on a bad response code.
  2815  //
  2816  // See https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
  2817  func (c *client) AddLabel(org, repo string, number int, label string) error {
  2818  	return c.AddLabelWithContext(context.Background(), org, repo, number, label)
  2819  }
  2820  
  2821  func (c *client) AddLabelWithContext(ctx context.Context, org, repo string, number int, label string) error {
  2822  	return c.AddLabelsWithContext(ctx, org, repo, number, label)
  2823  }
  2824  
  2825  // AddLabels adds one or more labels to org/repo#number, returning an error on a bad response code.
  2826  //
  2827  // See https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
  2828  func (c *client) AddLabels(org, repo string, number int, labels ...string) error {
  2829  	return c.AddLabelsWithContext(context.Background(), org, repo, number, labels...)
  2830  }
  2831  
  2832  func (c *client) AddLabelsWithContext(ctx context.Context, org, repo string, number int, labels ...string) error {
  2833  	durationLogger := c.log("AddLabels", org, repo, number, labels)
  2834  	defer durationLogger()
  2835  
  2836  	_, err := c.requestWithContext(ctx, &request{
  2837  		method:      http.MethodPost,
  2838  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d/labels", org, repo, number),
  2839  		org:         org,
  2840  		requestBody: labels,
  2841  		exitCodes:   []int{200},
  2842  	}, nil)
  2843  	return err
  2844  }
  2845  
  2846  type githubError struct {
  2847  	Message string `json:"message,omitempty"`
  2848  }
  2849  
  2850  // RemoveLabel removes label from org/repo#number, returning an error on any failure.
  2851  //
  2852  // See https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue
  2853  func (c *client) RemoveLabel(org, repo string, number int, label string) error {
  2854  	return c.RemoveLabelWithContext(context.Background(), org, repo, number, label)
  2855  }
  2856  
  2857  func (c *client) RemoveLabelWithContext(ctx context.Context, org, repo string, number int, label string) error {
  2858  	durationLogger := c.log("RemoveLabel", org, repo, number, label)
  2859  	defer durationLogger()
  2860  
  2861  	code, body, err := c.requestRawWithContext(ctx, &request{
  2862  		method: http.MethodDelete,
  2863  		path:   fmt.Sprintf("/repos/%s/%s/issues/%d/labels/%s", org, repo, number, label),
  2864  		org:    org,
  2865  		// GitHub sometimes returns 200 for this call, which is a bug on their end.
  2866  		// Do not expect a 404 exit code and handle it separately because we need
  2867  		// to introspect the request's response body.
  2868  		exitCodes: []int{200, 204},
  2869  	})
  2870  
  2871  	switch {
  2872  	case code == 200 || code == 204:
  2873  		// If our code was 200 or 204, no error info.
  2874  		return nil
  2875  	case code == 404:
  2876  		// continue
  2877  	case err != nil:
  2878  		return err
  2879  	default:
  2880  		return fmt.Errorf("unexpected status code: %d", code)
  2881  	}
  2882  
  2883  	ge := &githubError{}
  2884  	if err := json.Unmarshal(body, ge); err != nil {
  2885  		return err
  2886  	}
  2887  
  2888  	// If the error was because the label was not found, we don't really
  2889  	// care since the label won't exist anyway.
  2890  	if ge.Message == "Label does not exist" {
  2891  		return nil
  2892  	}
  2893  
  2894  	// Otherwise we got some other 404 error.
  2895  	return fmt.Errorf("deleting label 404: %s", ge.Message)
  2896  }
  2897  
  2898  func (c *client) WasLabelAddedByHuman(org, repo string, number int, label string) (bool, error) {
  2899  	isBot, err := c.BotUserChecker()
  2900  	if err != nil {
  2901  		return false, fmt.Errorf("failed to construct bot user checker: %w", err)
  2902  	}
  2903  
  2904  	events, err := c.ListIssueEvents(org, repo, number)
  2905  	if err != nil {
  2906  		return false, fmt.Errorf("failed to list issue events: %w", err)
  2907  	}
  2908  	var lastAdded ListedIssueEvent
  2909  	for _, event := range events {
  2910  		if event.Event != IssueActionLabeled || event.Label.Name != label {
  2911  			continue
  2912  		}
  2913  		lastAdded = event
  2914  	}
  2915  
  2916  	if lastAdded.Actor.Login == "" || isBot(lastAdded.Actor.Login) {
  2917  		return false, nil
  2918  	}
  2919  
  2920  	return true, nil
  2921  }
  2922  
  2923  // MissingUsers is an error specifying the users that could not be unassigned.
  2924  type MissingUsers struct {
  2925  	Users  []string
  2926  	action string
  2927  	apiErr error
  2928  }
  2929  
  2930  func (m MissingUsers) Error() string {
  2931  	return fmt.Sprintf("could not %s the following user(s): %s; %v.", m.action, strings.Join(m.Users, ", "), m.apiErr)
  2932  }
  2933  
  2934  // AssignIssue adds logins to org/repo#number, returning an error if any login is missing after making the call.
  2935  //
  2936  // See https://developer.github.com/v3/issues/assignees/#add-assignees-to-an-issue
  2937  func (c *client) AssignIssue(org, repo string, number int, logins []string) error {
  2938  	durationLogger := c.log("AssignIssue", org, repo, number, logins)
  2939  	defer durationLogger()
  2940  
  2941  	assigned := make(map[string]bool)
  2942  	var i Issue
  2943  	_, err := c.request(&request{
  2944  		method:      http.MethodPost,
  2945  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d/assignees", org, repo, number),
  2946  		org:         org,
  2947  		requestBody: map[string][]string{"assignees": logins},
  2948  		exitCodes:   []int{201},
  2949  	}, &i)
  2950  	if err != nil {
  2951  		return err
  2952  	}
  2953  	for _, assignee := range i.Assignees {
  2954  		assigned[NormLogin(assignee.Login)] = true
  2955  	}
  2956  	missing := MissingUsers{action: "assign"}
  2957  	for _, login := range logins {
  2958  		if !assigned[NormLogin(login)] {
  2959  			missing.Users = append(missing.Users, login)
  2960  		}
  2961  	}
  2962  	if len(missing.Users) > 0 {
  2963  		return missing
  2964  	}
  2965  	return nil
  2966  }
  2967  
  2968  // ExtraUsers is an error specifying the users that could not be unassigned.
  2969  type ExtraUsers struct {
  2970  	Users  []string
  2971  	action string
  2972  }
  2973  
  2974  func (e ExtraUsers) Error() string {
  2975  	return fmt.Sprintf("could not %s the following user(s): %s.", e.action, strings.Join(e.Users, ", "))
  2976  }
  2977  
  2978  // UnassignIssue removes logins from org/repo#number, returns an error if any login remains assigned.
  2979  //
  2980  // See https://developer.github.com/v3/issues/assignees/#remove-assignees-from-an-issue
  2981  func (c *client) UnassignIssue(org, repo string, number int, logins []string) error {
  2982  	durationLogger := c.log("UnassignIssue", org, repo, number, logins)
  2983  	defer durationLogger()
  2984  
  2985  	assigned := make(map[string]bool)
  2986  	var i Issue
  2987  	_, err := c.request(&request{
  2988  		method:      http.MethodDelete,
  2989  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d/assignees", org, repo, number),
  2990  		org:         org,
  2991  		requestBody: map[string][]string{"assignees": logins},
  2992  		exitCodes:   []int{200},
  2993  	}, &i)
  2994  	if err != nil {
  2995  		return err
  2996  	}
  2997  	for _, assignee := range i.Assignees {
  2998  		assigned[NormLogin(assignee.Login)] = true
  2999  	}
  3000  	extra := ExtraUsers{action: "unassign"}
  3001  	for _, login := range logins {
  3002  		if assigned[NormLogin(login)] {
  3003  			extra.Users = append(extra.Users, login)
  3004  		}
  3005  	}
  3006  	if len(extra.Users) > 0 {
  3007  		return extra
  3008  	}
  3009  	return nil
  3010  }
  3011  
  3012  // CreateReview creates a review using the draft.
  3013  //
  3014  // https://developer.github.com/v3/pulls/reviews/#create-a-pull-request-review
  3015  func (c *client) CreateReview(org, repo string, number int, r DraftReview) error {
  3016  	durationLogger := c.log("CreateReview", org, repo, number, r)
  3017  	defer durationLogger()
  3018  
  3019  	_, err := c.request(&request{
  3020  		method:      http.MethodPost,
  3021  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d/reviews", org, repo, number),
  3022  		accept:      "application/vnd.github.black-cat-preview+json",
  3023  		org:         org,
  3024  		requestBody: r,
  3025  		exitCodes:   []int{200},
  3026  	}, nil)
  3027  	return err
  3028  }
  3029  
  3030  // prepareReviewersBody separates reviewers from team_reviewers and prepares a map
  3031  //
  3032  //	{
  3033  //	  "reviewers": [
  3034  //	    "octocat",
  3035  //	    "hubot",
  3036  //	    "other_user"
  3037  //	  ],
  3038  //	  "team_reviewers": [
  3039  //	    "justice-league"
  3040  //	  ]
  3041  //	}
  3042  //
  3043  // https://developer.github.com/v3/pulls/review_requests/#create-a-review-request
  3044  func prepareReviewersBody(logins []string, org string) (map[string][]string, error) {
  3045  	body := map[string][]string{}
  3046  	var errors []error
  3047  	for _, login := range logins {
  3048  		mat := teamRe.FindStringSubmatch(login)
  3049  		if mat == nil {
  3050  			if _, exists := body["reviewers"]; !exists {
  3051  				body["reviewers"] = []string{}
  3052  			}
  3053  			body["reviewers"] = append(body["reviewers"], login)
  3054  		} else if mat[1] == org {
  3055  			if _, exists := body["team_reviewers"]; !exists {
  3056  				body["team_reviewers"] = []string{}
  3057  			}
  3058  			body["team_reviewers"] = append(body["team_reviewers"], mat[2])
  3059  		} else {
  3060  			errors = append(errors, fmt.Errorf("team %s is not part of %s org", login, org))
  3061  		}
  3062  	}
  3063  	return body, utilerrors.NewAggregate(errors)
  3064  }
  3065  
  3066  func (c *client) tryRequestReview(org, repo string, number int, logins []string) (int, error) {
  3067  	durationLogger := c.log("RequestReview", org, repo, number, logins)
  3068  	defer durationLogger()
  3069  
  3070  	var pr PullRequest
  3071  	body, err := prepareReviewersBody(logins, org)
  3072  	if err != nil {
  3073  		// At least one team not in org,
  3074  		// let RequestReview handle retries and alerting for each login.
  3075  		return http.StatusUnprocessableEntity, err
  3076  	}
  3077  	return c.request(&request{
  3078  		method:      http.MethodPost,
  3079  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d/requested_reviewers", org, repo, number),
  3080  		org:         org,
  3081  		accept:      "application/vnd.github.symmetra-preview+json",
  3082  		requestBody: body,
  3083  		exitCodes:   []int{http.StatusCreated /*201*/},
  3084  	}, &pr)
  3085  }
  3086  
  3087  // RequestReview tries to add the users listed in 'logins' as requested reviewers of the specified PR.
  3088  // If any user in the 'logins' slice is not a contributor of the repo, the entire POST will fail
  3089  // without adding any reviewers. The GitHub API response does not specify which user(s) were invalid
  3090  // so if we fail to request reviews from the members of 'logins' we try to request reviews from
  3091  // each member individually. We try first with all users in 'logins' for efficiency in the common case.
  3092  //
  3093  // See https://developer.github.com/v3/pulls/review_requests/#create-a-review-request
  3094  func (c *client) RequestReview(org, repo string, number int, logins []string) error {
  3095  	statusCode, err := c.tryRequestReview(org, repo, number, logins)
  3096  	if err != nil && statusCode == http.StatusUnprocessableEntity /*422*/ {
  3097  		// Failed to set all members of 'logins' as reviewers, try individually.
  3098  		missing := MissingUsers{action: "request a PR review from", apiErr: err}
  3099  		for _, user := range logins {
  3100  			statusCode, err = c.tryRequestReview(org, repo, number, []string{user})
  3101  			if err != nil && statusCode == http.StatusUnprocessableEntity /*422*/ {
  3102  				// User is not a contributor, or team not in org.
  3103  				missing.Users = append(missing.Users, user)
  3104  			} else if err != nil {
  3105  				return fmt.Errorf("failed to add reviewer to PR. Status code: %d, errmsg: %w", statusCode, err)
  3106  			}
  3107  		}
  3108  		if len(missing.Users) > 0 {
  3109  			return missing
  3110  		}
  3111  		return nil
  3112  	}
  3113  	return err
  3114  }
  3115  
  3116  // UnrequestReview tries to remove the users listed in 'logins' from the requested reviewers of the
  3117  // specified PR. The GitHub API treats deletions of review requests differently than creations. Specifically, if
  3118  // 'logins' contains a user that isn't a requested reviewer, other users that are valid are still removed.
  3119  // Furthermore, the API response lists the set of requested reviewers after the deletion (unlike request creations),
  3120  // so we can determine if each deletion was successful.
  3121  // The API responds with http status code 200 no matter what the content of 'logins' is.
  3122  //
  3123  // See https://developer.github.com/v3/pulls/review_requests/#delete-a-review-request
  3124  func (c *client) UnrequestReview(org, repo string, number int, logins []string) error {
  3125  	durationLogger := c.log("UnrequestReview", org, repo, number, logins)
  3126  	defer durationLogger()
  3127  
  3128  	var pr PullRequest
  3129  	body, err := prepareReviewersBody(logins, org)
  3130  	if len(body) == 0 {
  3131  		// No point in doing request for none,
  3132  		// if some logins didn't make it to body, extras.Users will catch them.
  3133  		return err
  3134  	}
  3135  	_, err = c.request(&request{
  3136  		method:      http.MethodDelete,
  3137  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d/requested_reviewers", org, repo, number),
  3138  		accept:      "application/vnd.github.symmetra-preview+json",
  3139  		org:         org,
  3140  		requestBody: body,
  3141  		exitCodes:   []int{http.StatusOK /*200*/},
  3142  	}, &pr)
  3143  	if err != nil {
  3144  		return err
  3145  	}
  3146  	extras := ExtraUsers{action: "remove the PR review request for"}
  3147  	for _, user := range pr.RequestedReviewers {
  3148  		found := false
  3149  		for _, toDelete := range logins {
  3150  			if NormLogin(user.Login) == NormLogin(toDelete) {
  3151  				found = true
  3152  				break
  3153  			}
  3154  		}
  3155  		if found {
  3156  			extras.Users = append(extras.Users, user.Login)
  3157  		}
  3158  	}
  3159  	if len(extras.Users) > 0 {
  3160  		return extras
  3161  	}
  3162  	return nil
  3163  }
  3164  
  3165  // CloseIssue closes the existing, open issue provided
  3166  // CloseIssue also closes the issue with the reason being
  3167  // "completed" - default value for the state_reason attribute.
  3168  //
  3169  // See https://developer.github.com/v3/issues/#edit-an-issue
  3170  func (c *client) CloseIssue(org, repo string, number int) error {
  3171  	durationLogger := c.log("CloseIssue", org, repo, number)
  3172  	defer durationLogger()
  3173  
  3174  	return c.closeIssue(org, repo, number, "completed")
  3175  }
  3176  
  3177  // CloseIssueAsNotPlanned closes the existing, open issue provided
  3178  // CloseIssueAsNotPlanned also closes the issue with the reason being
  3179  // "not_planned" - value passed for the state_reason attribute.
  3180  //
  3181  // See https://developer.github.com/v3/issues/#edit-an-issue
  3182  func (c *client) CloseIssueAsNotPlanned(org, repo string, number int) error {
  3183  	durationLogger := c.log("CloseIssueAsNotPlanned", org, repo, number)
  3184  	defer durationLogger()
  3185  
  3186  	return c.closeIssue(org, repo, number, "not_planned")
  3187  }
  3188  
  3189  func (c *client) closeIssue(org, repo string, number int, reason string) error {
  3190  	_, err := c.request(&request{
  3191  		method:      http.MethodPatch,
  3192  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d", org, repo, number),
  3193  		org:         org,
  3194  		requestBody: map[string]string{"state": "closed", "state_reason": reason},
  3195  		exitCodes:   []int{200},
  3196  	}, nil)
  3197  
  3198  	return err
  3199  }
  3200  
  3201  // StateCannotBeChanged represents the "custom" GitHub API
  3202  // error that occurs when a resource cannot be changed
  3203  type StateCannotBeChanged struct {
  3204  	Message string
  3205  }
  3206  
  3207  func (s StateCannotBeChanged) Error() string {
  3208  	return s.Message
  3209  }
  3210  
  3211  // StateCannotBeChanged implements error
  3212  var _ error = (*StateCannotBeChanged)(nil)
  3213  
  3214  // convert to a StateCannotBeChanged if appropriate or else return the original error
  3215  func stateCannotBeChangedOrOriginalError(err error) error {
  3216  	requestErr, ok := err.(requestError)
  3217  	if ok {
  3218  		for _, errorMsg := range requestErr.ErrorMessages() {
  3219  			if strings.Contains(errorMsg, stateCannotBeChangedMessagePrefix) {
  3220  				return StateCannotBeChanged{
  3221  					Message: errorMsg,
  3222  				}
  3223  			}
  3224  		}
  3225  	}
  3226  	return err
  3227  }
  3228  
  3229  // ReopenIssue re-opens the existing, closed issue provided
  3230  //
  3231  // See https://developer.github.com/v3/issues/#edit-an-issue
  3232  func (c *client) ReopenIssue(org, repo string, number int) error {
  3233  	durationLogger := c.log("ReopenIssue", org, repo, number)
  3234  	defer durationLogger()
  3235  
  3236  	_, err := c.request(&request{
  3237  		method:      http.MethodPatch,
  3238  		path:        fmt.Sprintf("/repos/%s/%s/issues/%d", org, repo, number),
  3239  		org:         org,
  3240  		requestBody: map[string]string{"state": "open"},
  3241  		exitCodes:   []int{200},
  3242  	}, nil)
  3243  	return stateCannotBeChangedOrOriginalError(err)
  3244  }
  3245  
  3246  // ClosePullRequest closes the existing, open PR provided
  3247  //
  3248  // See https://developer.github.com/v3/pulls/#update-a-pull-request
  3249  func (c *client) ClosePullRequest(org, repo string, number int) error {
  3250  	durationLogger := c.log("ClosePullRequest", org, repo, number)
  3251  	defer durationLogger()
  3252  
  3253  	_, err := c.request(&request{
  3254  		method:      http.MethodPatch,
  3255  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  3256  		org:         org,
  3257  		requestBody: map[string]string{"state": "closed"},
  3258  		exitCodes:   []int{200},
  3259  	}, nil)
  3260  	return err
  3261  }
  3262  
  3263  // ReopenPullRequest re-opens the existing, closed PR provided
  3264  //
  3265  // See https://developer.github.com/v3/pulls/#update-a-pull-request
  3266  func (c *client) ReopenPullRequest(org, repo string, number int) error {
  3267  	durationLogger := c.log("ReopenPullRequest", org, repo, number)
  3268  	defer durationLogger()
  3269  
  3270  	_, err := c.request(&request{
  3271  		method:      http.MethodPatch,
  3272  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d", org, repo, number),
  3273  		org:         org,
  3274  		requestBody: map[string]string{"state": "open"},
  3275  		exitCodes:   []int{200},
  3276  	}, nil)
  3277  	return stateCannotBeChangedOrOriginalError(err)
  3278  }
  3279  
  3280  // GetRef returns the SHA of the given ref, such as "heads/master".
  3281  //
  3282  // See https://developer.github.com/v3/git/refs/#get-a-reference
  3283  // The gitbub api does prefix matching and might return multiple results,
  3284  // in which case we will return a GetRefTooManyResultsError
  3285  func (c *client) GetRef(org, repo, ref string) (string, error) {
  3286  	durationLogger := c.log("GetRef", org, repo, ref)
  3287  	defer durationLogger()
  3288  
  3289  	res := GetRefResponse{}
  3290  	_, err := c.request(&request{
  3291  		method:    http.MethodGet,
  3292  		path:      fmt.Sprintf("/repos/%s/%s/git/refs/%s", org, repo, ref),
  3293  		org:       org,
  3294  		exitCodes: []int{200},
  3295  	}, &res)
  3296  	if err != nil {
  3297  		return "", err
  3298  	}
  3299  
  3300  	if n := len(res); n > 1 {
  3301  		wantRef := "refs/" + ref
  3302  		for _, r := range res {
  3303  			if r.Ref == wantRef {
  3304  				return r.Object.SHA, nil
  3305  			}
  3306  		}
  3307  		return "", GetRefTooManyResultsError{org: org, repo: repo, ref: ref, resultsRefs: res.RefNames()}
  3308  	}
  3309  	return res[0].Object.SHA, nil
  3310  }
  3311  
  3312  type GetRefTooManyResultsError struct {
  3313  	org, repo, ref string
  3314  	resultsRefs    []string
  3315  }
  3316  
  3317  func (GetRefTooManyResultsError) Is(err error) bool {
  3318  	_, ok := err.(GetRefTooManyResultsError)
  3319  	return ok
  3320  }
  3321  
  3322  func (e GetRefTooManyResultsError) Error() string {
  3323  	return fmt.Sprintf("query for %s/%s ref %q didn't match one but multiple refs: %v", e.org, e.repo, e.ref, e.resultsRefs)
  3324  }
  3325  
  3326  type GetRefResponse []GetRefResult
  3327  
  3328  // We need a custom unmarshaler because the GetRefResult may either be a
  3329  // single GetRefResponse or multiple
  3330  func (grr *GetRefResponse) UnmarshalJSON(data []byte) error {
  3331  	result := &GetRefResult{}
  3332  	if err := json.Unmarshal(data, result); err == nil {
  3333  		*(grr) = GetRefResponse{*result}
  3334  		return nil
  3335  	}
  3336  	var response []GetRefResult
  3337  	if err := json.Unmarshal(data, &response); err != nil {
  3338  		return fmt.Errorf("failed to unmarshal response %s: %w", string(data), err)
  3339  	}
  3340  	*grr = GetRefResponse(response)
  3341  	return nil
  3342  }
  3343  
  3344  func (grr *GetRefResponse) RefNames() []string {
  3345  	var result []string
  3346  	for _, item := range *grr {
  3347  		result = append(result, item.Ref)
  3348  	}
  3349  	return result
  3350  }
  3351  
  3352  type GetRefResult struct {
  3353  	Ref    string `json:"ref,omitempty"`
  3354  	NodeID string `json:"node_id,omitempty"`
  3355  	URL    string `json:"url,omitempty"`
  3356  	Object struct {
  3357  		Type string `json:"type,omitempty"`
  3358  		SHA  string `json:"sha,omitempty"`
  3359  		URL  string `json:"url,omitempty"`
  3360  	} `json:"object,omitempty"`
  3361  }
  3362  
  3363  // DeleteRef deletes the given ref
  3364  //
  3365  // See https://developer.github.com/v3/git/refs/#delete-a-reference
  3366  func (c *client) DeleteRef(org, repo, ref string) error {
  3367  	durationLogger := c.log("DeleteRef", org, repo, ref)
  3368  	defer durationLogger()
  3369  
  3370  	_, err := c.request(&request{
  3371  		method:    http.MethodDelete,
  3372  		path:      fmt.Sprintf("/repos/%s/%s/git/refs/%s", org, repo, ref),
  3373  		org:       org,
  3374  		exitCodes: []int{204},
  3375  	}, nil)
  3376  	return err
  3377  }
  3378  
  3379  // ListFileCommits returns the commits for this file path.
  3380  //
  3381  // See https://developer.github.com/v3/repos/#list-commits
  3382  func (c *client) ListFileCommits(org, repo, filePath string) ([]RepositoryCommit, error) {
  3383  	durationLogger := c.log("ListFileCommits", org, repo, filePath)
  3384  	defer durationLogger()
  3385  
  3386  	var commits []RepositoryCommit
  3387  	err := c.readPaginatedResultsWithValues(
  3388  		fmt.Sprintf("/repos/%s/%s/commits", org, repo),
  3389  		url.Values{
  3390  			"path":     []string{filePath},
  3391  			"per_page": []string{"100"},
  3392  		},
  3393  		acceptNone,
  3394  		org,
  3395  		func() interface{} { // newObj
  3396  			return &[]RepositoryCommit{}
  3397  		},
  3398  		func(obj interface{}) {
  3399  			commits = append(commits, *(obj.(*[]RepositoryCommit))...)
  3400  		},
  3401  	)
  3402  	if err != nil {
  3403  		return nil, err
  3404  	}
  3405  	return commits, nil
  3406  }
  3407  
  3408  // FindIssues uses the GitHub search API to find issues which match a particular query.
  3409  //
  3410  // Input query the same way you would into the website.
  3411  // Order returned results with sort (usually "updated").
  3412  // Control whether oldest/newest is first with asc.
  3413  // This method is not working in contexts where "github-app-id" is set. Please use FindIssuesWithOrg() in those cases.
  3414  //
  3415  // See https://help.github.com/articles/searching-issues-and-pull-requests/ for details.
  3416  func (c *client) FindIssues(query, sort string, asc bool) ([]Issue, error) {
  3417  	return c.FindIssuesWithOrg("", query, sort, asc)
  3418  }
  3419  
  3420  // FindIssuesWithOrg uses the GitHub search API to find issues which match a particular query.
  3421  //
  3422  // Input query the same way you would into the website.
  3423  // Order returned results with sort (usually "updated").
  3424  // Control whether oldest/newest is first with asc.
  3425  // This method is supposed to be used in contexts where "github-app-id" is set.
  3426  //
  3427  // See https://help.github.com/articles/searching-issues-and-pull-requests/ for details.
  3428  func (c *client) FindIssuesWithOrg(org, query, sort string, asc bool) ([]Issue, error) {
  3429  	loggerName := "FindIssuesWithOrg"
  3430  	if org == "" {
  3431  		loggerName = "FindIssues"
  3432  	}
  3433  	durationLogger := c.log(loggerName, query)
  3434  	defer durationLogger()
  3435  
  3436  	values := url.Values{
  3437  		"per_page": []string{"100"},
  3438  		"q":        []string{query},
  3439  	}
  3440  	var issues []Issue
  3441  
  3442  	if sort != "" {
  3443  		values["sort"] = []string{sort}
  3444  		if asc {
  3445  			values["order"] = []string{"asc"}
  3446  		}
  3447  	}
  3448  	err := c.readPaginatedResultsWithValues(
  3449  		"/search/issues",
  3450  		values,
  3451  		acceptNone,
  3452  		org,
  3453  		func() interface{} { // newObj
  3454  			return &IssuesSearchResult{}
  3455  		},
  3456  		func(obj interface{}) {
  3457  			issues = append(issues, obj.(*IssuesSearchResult).Issues...)
  3458  		},
  3459  	)
  3460  	if err != nil {
  3461  		return nil, err
  3462  	}
  3463  	return issues, err
  3464  }
  3465  
  3466  // FileNotFound happens when github cannot find the file requested by GetFile().
  3467  type FileNotFound struct {
  3468  	org, repo, path, commit string
  3469  }
  3470  
  3471  func (e *FileNotFound) Error() string {
  3472  	return fmt.Sprintf("%s/%s/%s @ %s not found", e.org, e.repo, e.path, e.commit)
  3473  }
  3474  
  3475  // GetFile uses GitHub repo contents API to retrieve the content of a file with commit SHA.
  3476  // If commit is empty, it will grab content from repo's default branch, usually master.
  3477  // Use GetDirectory() method to retrieve a directory.
  3478  //
  3479  // See https://developer.github.com/v3/repos/contents/#get-contents
  3480  func (c *client) GetFile(org, repo, filepath, commit string) ([]byte, error) {
  3481  	durationLogger := c.log("GetFile", org, repo, filepath, commit)
  3482  	defer durationLogger()
  3483  
  3484  	path := fmt.Sprintf("/repos/%s/%s/contents/%s", org, repo, filepath)
  3485  	if commit != "" {
  3486  		path = fmt.Sprintf("%s?ref=%s", path, url.QueryEscape(commit))
  3487  	}
  3488  
  3489  	var res Content
  3490  	code, err := c.request(&request{
  3491  		method:    http.MethodGet,
  3492  		path:      path,
  3493  		org:       org,
  3494  		exitCodes: []int{200, 404},
  3495  	}, &res)
  3496  
  3497  	if err != nil {
  3498  		return nil, err
  3499  	}
  3500  
  3501  	if code == 404 {
  3502  		return nil, &FileNotFound{
  3503  			org:    org,
  3504  			repo:   repo,
  3505  			path:   filepath,
  3506  			commit: commit,
  3507  		}
  3508  	}
  3509  
  3510  	decoded, err := base64.StdEncoding.DecodeString(res.Content)
  3511  	if err != nil {
  3512  		return nil, fmt.Errorf("error decoding %s : %w", res.Content, err)
  3513  	}
  3514  
  3515  	return decoded, nil
  3516  }
  3517  
  3518  // QueryWithGitHubAppsSupport runs a GraphQL query using shurcooL/githubql's client.
  3519  func (c *client) QueryWithGitHubAppsSupport(ctx context.Context, q interface{}, vars map[string]interface{}, org string) error {
  3520  	// Don't log query here because Query is typically called multiple times to get all pages.
  3521  	// Instead log once per search and include total search cost.
  3522  	return c.gqlc.QueryWithGitHubAppsSupport(ctx, q, vars, org)
  3523  }
  3524  
  3525  // MutateWithGitHubAppsSupport runs a GraphQL mutation using shurcooL/githubql's client.
  3526  func (c *client) MutateWithGitHubAppsSupport(ctx context.Context, m interface{}, input githubql.Input, vars map[string]interface{}, org string) error {
  3527  	return c.gqlc.MutateWithGitHubAppsSupport(ctx, m, input, vars, org)
  3528  }
  3529  
  3530  // CreateTeam adds a team with name to the org, returning a struct with the new ID.
  3531  //
  3532  // See https://developer.github.com/v3/teams/#create-team
  3533  func (c *client) CreateTeam(org string, team Team) (*Team, error) {
  3534  	durationLogger := c.log("CreateTeam", org, team)
  3535  	defer durationLogger()
  3536  
  3537  	if team.Name == "" {
  3538  		return nil, errors.New("team.Name must be non-empty")
  3539  	}
  3540  	if c.fake {
  3541  		return nil, nil
  3542  	} else if c.dry {
  3543  		// When in dry mode we need a believable slug to call corresponding methods for this team
  3544  		team.Slug = strings.ToLower(strings.ReplaceAll(team.Name, " ", "-"))
  3545  		return &team, nil
  3546  	}
  3547  	path := fmt.Sprintf("/orgs/%s/teams", org)
  3548  	var retTeam Team
  3549  	_, err := c.request(&request{
  3550  		method:      http.MethodPost,
  3551  		path:        path,
  3552  		accept:      "application/vnd.github+json",
  3553  		org:         org,
  3554  		requestBody: &team,
  3555  		exitCodes:   []int{201},
  3556  	}, &retTeam)
  3557  	return &retTeam, err
  3558  }
  3559  
  3560  // EditTeam patches team.Slug to contain the specified:
  3561  // name, description, privacy, permission, and parentTeamId values.
  3562  //
  3563  // See https://docs.github.com/en/rest/reference/teams#update-a-team
  3564  func (c *client) EditTeam(org string, t Team) (*Team, error) {
  3565  	durationLogger := c.log("EditTeam", t)
  3566  	defer durationLogger()
  3567  
  3568  	if t.Slug == "" {
  3569  		return nil, errors.New("team.Slug must be populated")
  3570  	}
  3571  	if c.dry {
  3572  		return &t, nil
  3573  	}
  3574  	t.ID = 0
  3575  	// Need to send parent_team_id: null
  3576  	team := struct {
  3577  		Team
  3578  		ParentTeamID *int `json:"parent_team_id"`
  3579  	}{
  3580  		Team:         t,
  3581  		ParentTeamID: t.ParentTeamID,
  3582  	}
  3583  	var retTeam Team
  3584  	path := fmt.Sprintf("/orgs/%s/teams/%s", org, t.Slug)
  3585  	_, err := c.request(&request{
  3586  		method:      http.MethodPatch,
  3587  		path:        path,
  3588  		accept:      "application/vnd.github+json",
  3589  		org:         org,
  3590  		requestBody: &team,
  3591  		exitCodes:   []int{200, 201},
  3592  	}, &retTeam)
  3593  	return &retTeam, err
  3594  }
  3595  
  3596  // DeleteTeamBySlug removes team.Slug from GitHub.
  3597  //
  3598  // See https://docs.github.com/en/rest/reference/teams#delete-a-team
  3599  func (c *client) DeleteTeamBySlug(org, teamSlug string) error {
  3600  	durationLogger := c.log("DeleteTeamBySlug", org, teamSlug)
  3601  	defer durationLogger()
  3602  	path := fmt.Sprintf("/orgs/%s/teams/%s", org, teamSlug)
  3603  	_, err := c.request(&request{
  3604  		method:    http.MethodDelete,
  3605  		path:      path,
  3606  		org:       org,
  3607  		exitCodes: []int{204},
  3608  	}, nil)
  3609  	return err
  3610  }
  3611  
  3612  // ListTeams gets a list of teams for the given org
  3613  //
  3614  // See https://developer.github.com/v3/teams/#list-teams
  3615  func (c *client) ListTeams(org string) ([]Team, error) {
  3616  	durationLogger := c.log("ListTeams", org)
  3617  	defer durationLogger()
  3618  
  3619  	if c.fake {
  3620  		return nil, nil
  3621  	}
  3622  	path := fmt.Sprintf("/orgs/%s/teams", org)
  3623  	var teams []Team
  3624  	err := c.readPaginatedResults(
  3625  		path,
  3626  		"application/vnd.github+json",
  3627  		org,
  3628  		func() interface{} {
  3629  			return &[]Team{}
  3630  		},
  3631  		func(obj interface{}) {
  3632  			teams = append(teams, *(obj.(*[]Team))...)
  3633  		},
  3634  	)
  3635  	if err != nil {
  3636  		return nil, err
  3637  	}
  3638  	return teams, nil
  3639  }
  3640  
  3641  // UpdateTeamMembershipBySlug adds the user to the team and/or updates their role in that team.
  3642  //
  3643  // If the user is not a member of the org, GitHub will invite them to become an outside collaborator, setting their status to pending.
  3644  //
  3645  // https://docs.github.com/en/rest/reference/teams#add-or-update-team-membership-for-a-user
  3646  func (c *client) UpdateTeamMembershipBySlug(org, teamSlug, user string, maintainer bool) (*TeamMembership, error) {
  3647  	durationLogger := c.log("UpdateTeamMembershipBySlug", org, teamSlug, user, maintainer)
  3648  	defer durationLogger()
  3649  
  3650  	if c.fake {
  3651  		return nil, nil
  3652  	}
  3653  	tm := TeamMembership{}
  3654  	if maintainer {
  3655  		tm.Role = RoleMaintainer
  3656  	} else {
  3657  		tm.Role = RoleMember
  3658  	}
  3659  
  3660  	if c.dry {
  3661  		return &tm, nil
  3662  	}
  3663  
  3664  	_, err := c.request(&request{
  3665  		method:      http.MethodPut,
  3666  		path:        fmt.Sprintf("/orgs/%s/teams/%s/memberships/%s", org, teamSlug, user),
  3667  		org:         org,
  3668  		requestBody: &tm,
  3669  		exitCodes:   []int{200},
  3670  	}, &tm)
  3671  	return &tm, err
  3672  }
  3673  
  3674  // RemoveTeamMembershipBySlug removes the user from the team (but not the org).
  3675  //
  3676  // https://docs.github.com/en/rest/reference/teams#remove-team-membership-for-a-user
  3677  func (c *client) RemoveTeamMembershipBySlug(org, teamSlug, user string) error {
  3678  	durationLogger := c.log("RemoveTeamMembershipBySlug", org, teamSlug, user)
  3679  	defer durationLogger()
  3680  
  3681  	if c.fake {
  3682  		return nil
  3683  	}
  3684  	_, err := c.request(&request{
  3685  		method:    http.MethodDelete,
  3686  		path:      fmt.Sprintf("/orgs/%s/teams/%s/memberships/%s", org, teamSlug, user),
  3687  		org:       org,
  3688  		exitCodes: []int{204},
  3689  	}, nil)
  3690  	return err
  3691  }
  3692  
  3693  // ListTeamMembers gets a list of team members for the given team id
  3694  //
  3695  // Role options are "all", "maintainer" and "member"
  3696  //
  3697  // https://developer.github.com/v3/teams/members/#list-team-members
  3698  // Deprecated: please use ListTeamMembersBySlug
  3699  func (c *client) ListTeamMembers(org string, id int, role string) ([]TeamMember, error) {
  3700  	c.logger.WithField("methodName", "ListTeamMembers").
  3701  		Warn("method is deprecated, please use ListTeamMembersBySlug")
  3702  	durationLogger := c.log("ListTeamMembers", id, role)
  3703  	defer durationLogger()
  3704  
  3705  	if c.fake {
  3706  		return nil, nil
  3707  	}
  3708  	path := fmt.Sprintf("/teams/%d/members", id)
  3709  	var teamMembers []TeamMember
  3710  	err := c.readPaginatedResultsWithValues(
  3711  		path,
  3712  		url.Values{
  3713  			"per_page": []string{"100"},
  3714  			"role":     []string{role},
  3715  		},
  3716  		"application/vnd.github+json",
  3717  		org,
  3718  		func() interface{} {
  3719  			return &[]TeamMember{}
  3720  		},
  3721  		func(obj interface{}) {
  3722  			teamMembers = append(teamMembers, *(obj.(*[]TeamMember))...)
  3723  		},
  3724  	)
  3725  	if err != nil {
  3726  		return nil, err
  3727  	}
  3728  	return teamMembers, nil
  3729  }
  3730  
  3731  // ListTeamMembersBySlug gets a list of team members for the given team slug
  3732  //
  3733  // Role options are "all", "maintainer" and "member"
  3734  //
  3735  // https://docs.github.com/en/rest/reference/teams#list-team-members
  3736  func (c *client) ListTeamMembersBySlug(org, teamSlug, role string) ([]TeamMember, error) {
  3737  	durationLogger := c.log("ListTeamMembersBySlug", org, teamSlug, role)
  3738  	defer durationLogger()
  3739  
  3740  	if c.fake {
  3741  		return nil, nil
  3742  	}
  3743  	path := fmt.Sprintf("/orgs/%s/teams/%s/members", org, teamSlug)
  3744  	var teamMembers []TeamMember
  3745  	err := c.readPaginatedResultsWithValues(
  3746  		path,
  3747  		url.Values{
  3748  			"per_page": []string{"100"},
  3749  			"role":     []string{role},
  3750  		},
  3751  		"application/vnd.github.v3+json",
  3752  		org,
  3753  		func() interface{} {
  3754  			return &[]TeamMember{}
  3755  		},
  3756  		func(obj interface{}) {
  3757  			teamMembers = append(teamMembers, *(obj.(*[]TeamMember))...)
  3758  		},
  3759  	)
  3760  	if err != nil {
  3761  		return nil, err
  3762  	}
  3763  	return teamMembers, nil
  3764  }
  3765  
  3766  // ListTeamReposBySlug gets a list of team repos for the given team slug
  3767  //
  3768  // https://docs.github.com/en/rest/reference/teams#list-team-repositories
  3769  func (c *client) ListTeamReposBySlug(org, teamSlug string) ([]Repo, error) {
  3770  	durationLogger := c.log("ListTeamReposBySlug", org, teamSlug)
  3771  	defer durationLogger()
  3772  
  3773  	if c.fake {
  3774  		return nil, nil
  3775  	}
  3776  	path := fmt.Sprintf("/orgs/%s/teams/%s/repos", org, teamSlug)
  3777  	var repos []Repo
  3778  	err := c.readPaginatedResultsWithValues(
  3779  		path,
  3780  		url.Values{
  3781  			"per_page": []string{"100"},
  3782  		},
  3783  		"application/vnd.github.v3+json",
  3784  		org,
  3785  		func() interface{} {
  3786  			return &[]Repo{}
  3787  		},
  3788  		func(obj interface{}) {
  3789  			for _, repo := range *obj.(*[]Repo) {
  3790  				// Currently, GitHub API returns false for all permission levels
  3791  				// for a repo on which the team has 'Maintain' or 'Triage' role.
  3792  				// This check is to avoid listing a repo under the team but
  3793  				// showing the permission level as none.
  3794  				if LevelFromPermissions(repo.Permissions) != None {
  3795  					repos = append(repos, repo)
  3796  				}
  3797  			}
  3798  		},
  3799  	)
  3800  	if err != nil {
  3801  		return nil, err
  3802  	}
  3803  	return repos, nil
  3804  }
  3805  
  3806  // UpdateTeamRepoBySlug adds the repo to the team with the provided role.
  3807  //
  3808  // https://docs.github.com/en/rest/reference/teams#add-or-update-team-repository-permissions
  3809  func (c *client) UpdateTeamRepoBySlug(org, teamSlug, repo string, permission TeamPermission) error {
  3810  	durationLogger := c.log("UpdateTeamRepoBySlug", org, teamSlug, repo, permission)
  3811  	defer durationLogger()
  3812  
  3813  	if c.fake || c.dry {
  3814  		return nil
  3815  	}
  3816  
  3817  	data := struct {
  3818  		Permission string `json:"permission"`
  3819  	}{
  3820  		Permission: string(permission),
  3821  	}
  3822  
  3823  	_, err := c.request(&request{
  3824  		method:      http.MethodPut,
  3825  		path:        fmt.Sprintf("/orgs/%s/teams/%s/repos/%s/%s", org, teamSlug, org, repo),
  3826  		org:         org,
  3827  		requestBody: &data,
  3828  		exitCodes:   []int{204},
  3829  	}, nil)
  3830  	return err
  3831  }
  3832  
  3833  // RemoveTeamRepoBySlug removes the team from the repo.
  3834  //
  3835  // https://docs.github.com/en/rest/reference/teams#remove-a-repository-from-a-team
  3836  func (c *client) RemoveTeamRepoBySlug(org, teamSlug, repo string) error {
  3837  	durationLogger := c.log("RemoveTeamRepoBySlug", org, teamSlug, repo)
  3838  	defer durationLogger()
  3839  
  3840  	if c.fake || c.dry {
  3841  		return nil
  3842  	}
  3843  
  3844  	_, err := c.request(&request{
  3845  		method:    http.MethodDelete,
  3846  		path:      fmt.Sprintf("/orgs/%s/teams/%s/repos/%s/%s", org, teamSlug, org, repo),
  3847  		org:       org,
  3848  		exitCodes: []int{204},
  3849  	}, nil)
  3850  	return err
  3851  }
  3852  
  3853  // ListTeamInvitationsBySlug gets a list of team members with pending invitations for the given team slug
  3854  //
  3855  // https://docs.github.com/en/rest/reference/teams#list-pending-team-invitations
  3856  func (c *client) ListTeamInvitationsBySlug(org, teamSlug string) ([]OrgInvitation, error) {
  3857  	durationLogger := c.log("ListTeamInvitationsBySlug", org, teamSlug)
  3858  	defer durationLogger()
  3859  
  3860  	if c.fake {
  3861  		return nil, nil
  3862  	}
  3863  
  3864  	path := fmt.Sprintf("/orgs/%s/teams/%s/invitations", org, teamSlug)
  3865  	var ret []OrgInvitation
  3866  	err := c.readPaginatedResults(
  3867  		path,
  3868  		"application/vnd.github.v3+json",
  3869  		org,
  3870  		func() interface{} {
  3871  			return &[]OrgInvitation{}
  3872  		},
  3873  		func(obj interface{}) {
  3874  			ret = append(ret, *(obj.(*[]OrgInvitation))...)
  3875  		},
  3876  	)
  3877  	if err != nil {
  3878  		return nil, err
  3879  	}
  3880  	return ret, nil
  3881  }
  3882  
  3883  // MergeDetails contains desired properties of the merge.
  3884  //
  3885  // See https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
  3886  type MergeDetails struct {
  3887  	// CommitTitle defaults to the automatic message.
  3888  	CommitTitle string `json:"commit_title,omitempty"`
  3889  	// CommitMessage defaults to the automatic message.
  3890  	CommitMessage string `json:"commit_message,omitempty"`
  3891  	// The PR HEAD must match this to prevent races.
  3892  	SHA string `json:"sha,omitempty"`
  3893  	// Can be "merge", "squash", or "rebase". Defaults to merge.
  3894  	MergeMethod string `json:"merge_method,omitempty"`
  3895  }
  3896  
  3897  // ModifiedHeadError happens when github refuses to merge a PR because the PR changed.
  3898  type ModifiedHeadError string
  3899  
  3900  func (e ModifiedHeadError) Error() string { return string(e) }
  3901  
  3902  // UnmergablePRError happens when github refuses to merge a PR for other reasons (merge confclit).
  3903  type UnmergablePRError string
  3904  
  3905  func (e UnmergablePRError) Error() string { return string(e) }
  3906  
  3907  // UnmergablePRBaseChangedError happens when github refuses merging a PR because the base changed.
  3908  type UnmergablePRBaseChangedError string
  3909  
  3910  func (e UnmergablePRBaseChangedError) Error() string { return string(e) }
  3911  
  3912  // UnauthorizedToPushError happens when client is not allowed to push to github.
  3913  type UnauthorizedToPushError string
  3914  
  3915  func (e UnauthorizedToPushError) Error() string { return string(e) }
  3916  
  3917  // MergeCommitsForbiddenError happens when the repo disallows the merge strategy configured for the repo in Tide.
  3918  type MergeCommitsForbiddenError string
  3919  
  3920  func (e MergeCommitsForbiddenError) Error() string { return string(e) }
  3921  
  3922  // Merge merges a PR.
  3923  //
  3924  // See https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button
  3925  func (c *client) Merge(org, repo string, pr int, details MergeDetails) error {
  3926  	durationLogger := c.log("Merge", org, repo, pr, details)
  3927  	defer durationLogger()
  3928  
  3929  	ge := githubError{}
  3930  	ec, err := c.request(&request{
  3931  		method:      http.MethodPut,
  3932  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", org, repo, pr),
  3933  		org:         org,
  3934  		requestBody: &details,
  3935  		exitCodes:   []int{200, 405, 409},
  3936  	}, &ge)
  3937  	if err != nil {
  3938  		return err
  3939  	}
  3940  	if ec == 405 {
  3941  		if strings.Contains(ge.Message, "Base branch was modified") {
  3942  			return UnmergablePRBaseChangedError(ge.Message)
  3943  		}
  3944  		if strings.Contains(ge.Message, "You're not authorized to push to this branch") {
  3945  			return UnauthorizedToPushError(ge.Message)
  3946  		}
  3947  		if strings.Contains(ge.Message, "Merge commits are not allowed on this repository") {
  3948  			return MergeCommitsForbiddenError(ge.Message)
  3949  		}
  3950  		return UnmergablePRError(ge.Message)
  3951  	} else if ec == 409 {
  3952  		return ModifiedHeadError(ge.Message)
  3953  	}
  3954  
  3955  	return nil
  3956  }
  3957  
  3958  // IsCollaborator returns whether or not the user is a collaborator of the repo.
  3959  // From GitHub's API reference:
  3960  // For organization-owned repositories, the list of collaborators includes
  3961  // outside collaborators, organization members that are direct collaborators,
  3962  // organization members with access through team memberships, organization
  3963  // members with access through default organization permissions, and
  3964  // organization owners.
  3965  //
  3966  // See https://developer.github.com/v3/repos/collaborators/
  3967  func (c *client) IsCollaborator(org, repo, user string) (bool, error) {
  3968  	// This call does not support etags and is therefore not cacheable today
  3969  	// by ghproxy. If we can detect that we're using ghproxy, however, we can
  3970  	// make a more expensive but cache-able call instead. Detecting that we
  3971  	// are pointed at a ghproxy instance is not high fidelity, but a best-effort
  3972  	// approach here is guaranteed to make a positive impact and no negative one.
  3973  	if strings.Contains(c.bases[0], "ghproxy") {
  3974  		users, err := c.ListCollaborators(org, repo)
  3975  		if err != nil {
  3976  			return false, err
  3977  		}
  3978  		for _, u := range users {
  3979  			if NormLogin(u.Login) == NormLogin(user) {
  3980  				return true, nil
  3981  			}
  3982  		}
  3983  		return false, nil
  3984  	}
  3985  	durationLogger := c.log("IsCollaborator", org, user)
  3986  	defer durationLogger()
  3987  
  3988  	if org == user {
  3989  		// Make it possible to run a couple of plugins on personal repos.
  3990  		return true, nil
  3991  	}
  3992  	code, err := c.request(&request{
  3993  		method:    http.MethodGet,
  3994  		accept:    "application/vnd.github+json",
  3995  		path:      fmt.Sprintf("/repos/%s/%s/collaborators/%s", org, repo, user),
  3996  		org:       org,
  3997  		exitCodes: []int{204, 404, 302},
  3998  	}, nil)
  3999  	if err != nil {
  4000  		return false, err
  4001  	}
  4002  	if code == 204 {
  4003  		return true, nil
  4004  	} else if code == 404 {
  4005  		return false, nil
  4006  	}
  4007  	return false, fmt.Errorf("unexpected status: %d", code)
  4008  }
  4009  
  4010  // ListCollaborators gets a list of all users who have access to a repo (and
  4011  // can become assignees or requested reviewers).
  4012  //
  4013  // See 'IsCollaborator' for more details.
  4014  // See https://developer.github.com/v3/repos/collaborators/
  4015  func (c *client) ListCollaborators(org, repo string) ([]User, error) {
  4016  	durationLogger := c.log("ListCollaborators", org, repo)
  4017  	defer durationLogger()
  4018  
  4019  	if c.fake {
  4020  		return nil, nil
  4021  	}
  4022  	path := fmt.Sprintf("/repos/%s/%s/collaborators", org, repo)
  4023  	var users []User
  4024  	err := c.readPaginatedResults(
  4025  		path,
  4026  		"application/vnd.github+json",
  4027  		org,
  4028  		func() interface{} {
  4029  			return &[]User{}
  4030  		},
  4031  		func(obj interface{}) {
  4032  			users = append(users, *(obj.(*[]User))...)
  4033  		},
  4034  	)
  4035  	if err != nil {
  4036  		return nil, err
  4037  	}
  4038  	return users, nil
  4039  }
  4040  
  4041  // CreateFork creates a fork for the authenticated user. Forking a repository
  4042  // happens asynchronously. Therefore, we may have to wait a short period before
  4043  // accessing the git objects. If this takes longer than 5 minutes, GitHub
  4044  // recommends contacting their support.
  4045  //
  4046  // See https://developer.github.com/v3/repos/forks/#create-a-fork
  4047  func (c *client) CreateFork(owner, repo string) (string, error) {
  4048  	durationLogger := c.log("CreateFork", owner, repo)
  4049  	defer durationLogger()
  4050  
  4051  	resp := struct {
  4052  		Name string `json:"name"`
  4053  	}{}
  4054  
  4055  	_, err := c.request(&request{
  4056  		method:    http.MethodPost,
  4057  		path:      fmt.Sprintf("/repos/%s/%s/forks", owner, repo),
  4058  		org:       owner,
  4059  		exitCodes: []int{202},
  4060  	}, &resp)
  4061  
  4062  	// there are many reasons why GitHub may end up forking the
  4063  	// repo under a different name -- the repo got re-named, the
  4064  	// bot account already has a fork with that name, etc
  4065  	return resp.Name, err
  4066  }
  4067  
  4068  // EnsureFork checks to see that there is a fork of org/repo in the forkedUsers repositories.
  4069  // If there is not, it makes one, and waits for the fork to be created before returning.
  4070  // The return value is the name of the repo that was created
  4071  // (This may be different then the one that is forked due to naming conflict)
  4072  func (c *client) EnsureFork(forkingUser, org, repo string) (string, error) {
  4073  	// Fork repo if it doesn't exist.
  4074  	fork := forkingUser + "/" + repo
  4075  	repos, err := c.GetRepos(forkingUser, true)
  4076  	if err != nil {
  4077  		return repo, fmt.Errorf("could not fetch all existing repos: %w", err)
  4078  	}
  4079  	// if the repo does not exist, or it does, but is not a fork of the repo we want
  4080  	if forkedRepo := getFork(fork, repos); forkedRepo == nil || forkedRepo.Parent.FullName != fmt.Sprintf("%s/%s", org, repo) {
  4081  		if name, err := c.CreateFork(org, repo); err != nil {
  4082  			return repo, fmt.Errorf("cannot fork %s/%s: %w", org, repo, err)
  4083  		} else {
  4084  			// we got a fork but it may be named differently
  4085  			repo = name
  4086  		}
  4087  		if err := c.waitForRepo(forkingUser, repo); err != nil {
  4088  			return repo, fmt.Errorf("fork of %s/%s cannot show up on GitHub: %w", org, repo, err)
  4089  		}
  4090  	}
  4091  	return repo, nil
  4092  
  4093  }
  4094  
  4095  func (c *client) waitForRepo(owner, name string) error {
  4096  	// Wait for at most 5 minutes for the fork to appear on GitHub.
  4097  	// The documentation instructs us to contact support if this
  4098  	// takes longer than five minutes.
  4099  	after := time.After(6 * time.Minute)
  4100  	tick := time.NewTicker(30 * time.Second)
  4101  
  4102  	var ghErr string
  4103  	for {
  4104  		select {
  4105  		case <-tick.C:
  4106  			repo, err := c.GetRepo(owner, name)
  4107  			if err != nil {
  4108  				ghErr = fmt.Sprintf(": %v", err)
  4109  				logrus.WithError(err).Warn("Error getting bot repository.")
  4110  				continue
  4111  			}
  4112  			ghErr = ""
  4113  			if forkedRepo := getFork(owner+"/"+name, []Repo{repo.Repo}); forkedRepo != nil {
  4114  				return nil
  4115  			}
  4116  		case <-after:
  4117  			return fmt.Errorf("timed out waiting for %s to appear on GitHub%s", owner+"/"+name, ghErr)
  4118  		}
  4119  	}
  4120  }
  4121  
  4122  func getFork(repo string, repos []Repo) *Repo {
  4123  	for _, r := range repos {
  4124  		if !r.Fork {
  4125  			continue
  4126  		}
  4127  		if r.FullName == repo {
  4128  			return &r
  4129  		}
  4130  	}
  4131  	return nil
  4132  }
  4133  
  4134  // ListRepoTeams gets a list of all the teams with access to a repository
  4135  // See https://developer.github.com/v3/repos/#list-teams
  4136  func (c *client) ListRepoTeams(org, repo string) ([]Team, error) {
  4137  	durationLogger := c.log("ListRepoTeams", org, repo)
  4138  	defer durationLogger()
  4139  
  4140  	if c.fake {
  4141  		return nil, nil
  4142  	}
  4143  	path := fmt.Sprintf("/repos/%s/%s/teams", org, repo)
  4144  	var teams []Team
  4145  	err := c.readPaginatedResults(
  4146  		path,
  4147  		acceptNone,
  4148  		org,
  4149  		func() interface{} {
  4150  			return &[]Team{}
  4151  		},
  4152  		func(obj interface{}) {
  4153  			teams = append(teams, *(obj.(*[]Team))...)
  4154  		},
  4155  	)
  4156  	if err != nil {
  4157  		return nil, err
  4158  	}
  4159  	return teams, nil
  4160  }
  4161  
  4162  // ListIssueEvents gets a list events from GitHub's events API that pertain to the specified issue.
  4163  // The events that are returned have a different format than webhook events and certain event types
  4164  // are excluded.
  4165  //
  4166  // See https://developer.github.com/v3/issues/events/
  4167  func (c *client) ListIssueEvents(org, repo string, num int) ([]ListedIssueEvent, error) {
  4168  	durationLogger := c.log("ListIssueEvents", org, repo, num)
  4169  	defer durationLogger()
  4170  
  4171  	if c.fake {
  4172  		return nil, nil
  4173  	}
  4174  	path := fmt.Sprintf("/repos/%s/%s/issues/%d/events", org, repo, num)
  4175  	var events []ListedIssueEvent
  4176  	err := c.readPaginatedResults(
  4177  		path,
  4178  		acceptNone,
  4179  		org,
  4180  		func() interface{} {
  4181  			return &[]ListedIssueEvent{}
  4182  		},
  4183  		func(obj interface{}) {
  4184  			events = append(events, *(obj.(*[]ListedIssueEvent))...)
  4185  		},
  4186  	)
  4187  	if err != nil {
  4188  		return nil, err
  4189  	}
  4190  	return events, nil
  4191  }
  4192  
  4193  // IsMergeable determines if a PR can be merged.
  4194  // Mergeability is calculated by a background job on GitHub and is not immediately available when
  4195  // new commits are added so the PR must be polled until the background job completes.
  4196  func (c *client) IsMergeable(org, repo string, number int, SHA string) (bool, error) {
  4197  	backoff := time.Second * 3
  4198  	maxTries := 3
  4199  	for try := 0; try < maxTries; try++ {
  4200  		pr, err := c.GetPullRequest(org, repo, number)
  4201  		if err != nil {
  4202  			return false, err
  4203  		}
  4204  		if pr.Head.SHA != SHA {
  4205  			return false, fmt.Errorf("pull request head changed while checking mergeability (%s -> %s)", SHA, pr.Head.SHA)
  4206  		}
  4207  		if pr.Merged {
  4208  			return false, errors.New("pull request was merged while checking mergeability")
  4209  		}
  4210  		if pr.Mergable != nil {
  4211  			return *pr.Mergable, nil
  4212  		}
  4213  		if try+1 < maxTries {
  4214  			c.time.Sleep(backoff)
  4215  			backoff *= 2
  4216  		}
  4217  	}
  4218  	return false, fmt.Errorf("reached maximum number of retries (%d) checking mergeability", maxTries)
  4219  }
  4220  
  4221  // ClearMilestone clears the milestone from the specified issue
  4222  //
  4223  // See https://developer.github.com/v3/issues/#edit-an-issue
  4224  func (c *client) ClearMilestone(org, repo string, num int) error {
  4225  	durationLogger := c.log("ClearMilestone", org, repo, num)
  4226  	defer durationLogger()
  4227  
  4228  	issue := &struct {
  4229  		// Clearing the milestone requires providing a null value, and
  4230  		// interface{} will serialize to null.
  4231  		Milestone interface{} `json:"milestone"`
  4232  	}{}
  4233  	_, err := c.request(&request{
  4234  		method:      http.MethodPatch,
  4235  		path:        fmt.Sprintf("/repos/%v/%v/issues/%d", org, repo, num),
  4236  		org:         org,
  4237  		requestBody: &issue,
  4238  		exitCodes:   []int{200},
  4239  	}, nil)
  4240  	return err
  4241  }
  4242  
  4243  // SetMilestone sets the milestone from the specified issue (if it is a valid milestone)
  4244  //
  4245  // See https://developer.github.com/v3/issues/#edit-an-issue
  4246  func (c *client) SetMilestone(org, repo string, issueNum, milestoneNum int) error {
  4247  	durationLogger := c.log("SetMilestone", org, repo, issueNum, milestoneNum)
  4248  	defer durationLogger()
  4249  
  4250  	issue := &struct {
  4251  		Milestone int `json:"milestone"`
  4252  	}{Milestone: milestoneNum}
  4253  
  4254  	_, err := c.request(&request{
  4255  		method:      http.MethodPatch,
  4256  		path:        fmt.Sprintf("/repos/%v/%v/issues/%d", org, repo, issueNum),
  4257  		org:         org,
  4258  		requestBody: &issue,
  4259  		exitCodes:   []int{200},
  4260  	}, nil)
  4261  	return err
  4262  }
  4263  
  4264  // ListMilestones list all milestones in a repo
  4265  //
  4266  // See https://developer.github.com/v3/issues/milestones/#list-milestones-for-a-repository/
  4267  func (c *client) ListMilestones(org, repo string) ([]Milestone, error) {
  4268  	durationLogger := c.log("ListMilestones", org)
  4269  	defer durationLogger()
  4270  
  4271  	if c.fake {
  4272  		return nil, nil
  4273  	}
  4274  	path := fmt.Sprintf("/repos/%s/%s/milestones", org, repo)
  4275  	var milestones []Milestone
  4276  	err := c.readPaginatedResults(
  4277  		path,
  4278  		acceptNone,
  4279  		org,
  4280  		func() interface{} {
  4281  			return &[]Milestone{}
  4282  		},
  4283  		func(obj interface{}) {
  4284  			milestones = append(milestones, *(obj.(*[]Milestone))...)
  4285  		},
  4286  	)
  4287  	if err != nil {
  4288  		return nil, err
  4289  	}
  4290  	return milestones, nil
  4291  }
  4292  
  4293  // ListPullRequestCommits lists the commits in a pull request.
  4294  //
  4295  // GitHub API docs: https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
  4296  func (c *client) ListPullRequestCommits(org, repo string, number int) ([]RepositoryCommit, error) {
  4297  	durationLogger := c.log("ListPullRequestCommits", org, repo, number)
  4298  	defer durationLogger()
  4299  
  4300  	if c.fake {
  4301  		return nil, nil
  4302  	}
  4303  	var commits []RepositoryCommit
  4304  	err := c.readPaginatedResults(
  4305  		fmt.Sprintf("/repos/%v/%v/pulls/%d/commits", org, repo, number),
  4306  		acceptNone,
  4307  		org,
  4308  		func() interface{} { // newObj returns a pointer to the type of object to create
  4309  			return &[]RepositoryCommit{}
  4310  		},
  4311  		func(obj interface{}) { // accumulate is the accumulation function for paginated results
  4312  			commits = append(commits, *(obj.(*[]RepositoryCommit))...)
  4313  		},
  4314  	)
  4315  	if err != nil {
  4316  		return nil, err
  4317  	}
  4318  	return commits, nil
  4319  }
  4320  
  4321  // UpdatePullRequestBranch updates the pull request branch with the latest upstream changes by merging HEAD from the base branch into the pull request branch.
  4322  //
  4323  // GitHub API docs: https://developer.github.com/v3/pulls#update-a-pull-request-branch
  4324  func (c *client) UpdatePullRequestBranch(org, repo string, number int, expectedHeadSha *string) error {
  4325  	durationLogger := c.log("UpdatePullRequestBranch", org, repo)
  4326  	defer durationLogger()
  4327  
  4328  	data := struct {
  4329  		// The expected SHA of the pull request's HEAD ref. This is the most recent commit on the pull request's branch.
  4330  		// If the expected SHA does not match the pull request's HEAD, you will receive a 422 Unprocessable Entity status.
  4331  		// You can use the "List commits" endpoint to find the most recent commit SHA. Default: SHA of the pull request's current HEAD ref.
  4332  		ExpectedHeadSha *string `json:"expected_head_sha,omitempty"`
  4333  	}{
  4334  		ExpectedHeadSha: expectedHeadSha,
  4335  	}
  4336  
  4337  	code, err := c.request(&request{
  4338  		method:      http.MethodPut,
  4339  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d/update-branch", org, repo, number),
  4340  		accept:      "application/vnd.github.lydian-preview+json",
  4341  		org:         org,
  4342  		requestBody: &data,
  4343  		exitCodes:   []int{202, 422},
  4344  	}, nil)
  4345  	if err != nil {
  4346  		return err
  4347  	}
  4348  
  4349  	if code == http.StatusUnprocessableEntity {
  4350  		msg := "mismatch expected head sha"
  4351  		if expectedHeadSha != nil {
  4352  			msg = fmt.Sprintf("%s: %s", msg, *expectedHeadSha)
  4353  		}
  4354  		return errors.New(msg)
  4355  	}
  4356  
  4357  	return nil
  4358  }
  4359  
  4360  // newReloadingTokenSource creates a reloadingTokenSource.
  4361  func newReloadingTokenSource(getToken func() []byte) *reloadingTokenSource {
  4362  	return &reloadingTokenSource{
  4363  		getToken: getToken,
  4364  	}
  4365  }
  4366  
  4367  // Token is an implementation for oauth2.TokenSource interface.
  4368  func (s *reloadingTokenSource) Token() (*oauth2.Token, error) {
  4369  	return &oauth2.Token{
  4370  		AccessToken: string(s.getToken()),
  4371  	}, nil
  4372  }
  4373  
  4374  // GetRepoProjects returns the list of projects in this repo.
  4375  //
  4376  // See https://developer.github.com/v3/projects/#list-repository-projects
  4377  func (c *client) GetRepoProjects(owner, repo string) ([]Project, error) {
  4378  	durationLogger := c.log("GetOrgProjects", owner, repo)
  4379  	defer durationLogger()
  4380  
  4381  	path := fmt.Sprintf("/repos/%s/%s/projects", owner, repo)
  4382  	var projects []Project
  4383  	err := c.readPaginatedResults(
  4384  		path,
  4385  		"application/vnd.github.inertia-preview+json",
  4386  		owner,
  4387  		func() interface{} {
  4388  			return &[]Project{}
  4389  		},
  4390  		func(obj interface{}) {
  4391  			projects = append(projects, *(obj.(*[]Project))...)
  4392  		},
  4393  	)
  4394  	if err != nil {
  4395  		return nil, err
  4396  	}
  4397  	return projects, nil
  4398  }
  4399  
  4400  // GetOrgProjects returns the list of projects in this org.
  4401  //
  4402  // See https://developer.github.com/v3/projects/#list-organization-projects
  4403  func (c *client) GetOrgProjects(org string) ([]Project, error) {
  4404  	durationLogger := c.log("GetOrgProjects", org)
  4405  	defer durationLogger()
  4406  
  4407  	path := fmt.Sprintf("/orgs/%s/projects", org)
  4408  	var projects []Project
  4409  	err := c.readPaginatedResults(
  4410  		path,
  4411  		"application/vnd.github.inertia-preview+json",
  4412  		org,
  4413  		func() interface{} {
  4414  			return &[]Project{}
  4415  		},
  4416  		func(obj interface{}) {
  4417  			projects = append(projects, *(obj.(*[]Project))...)
  4418  		},
  4419  	)
  4420  	if err != nil {
  4421  		return nil, err
  4422  	}
  4423  	return projects, nil
  4424  }
  4425  
  4426  // GetProjectColumns returns the list of columns in a project.
  4427  //
  4428  // See https://developer.github.com/v3/projects/columns/#list-project-columns
  4429  func (c *client) GetProjectColumns(org string, projectID int) ([]ProjectColumn, error) {
  4430  	durationLogger := c.log("GetProjectColumns", projectID)
  4431  	defer durationLogger()
  4432  
  4433  	path := fmt.Sprintf("/projects/%d/columns", projectID)
  4434  	var projectColumns []ProjectColumn
  4435  	err := c.readPaginatedResults(
  4436  		path,
  4437  		"application/vnd.github.inertia-preview+json",
  4438  		org,
  4439  		func() interface{} {
  4440  			return &[]ProjectColumn{}
  4441  		},
  4442  		func(obj interface{}) {
  4443  			projectColumns = append(projectColumns, *(obj.(*[]ProjectColumn))...)
  4444  		},
  4445  	)
  4446  	if err != nil {
  4447  		return nil, err
  4448  	}
  4449  	return projectColumns, nil
  4450  }
  4451  
  4452  // CreateProjectCard adds a project card to the specified project column.
  4453  //
  4454  // See https://developer.github.com/v3/projects/cards/#create-a-project-card
  4455  func (c *client) CreateProjectCard(org string, columnID int, projectCard ProjectCard) (*ProjectCard, error) {
  4456  	durationLogger := c.log("CreateProjectCard", columnID, projectCard)
  4457  	defer durationLogger()
  4458  
  4459  	if (projectCard.ContentType != "Issue") && (projectCard.ContentType != "PullRequest") {
  4460  		return nil, errors.New("projectCard.ContentType must be either Issue or PullRequest")
  4461  	}
  4462  	if c.dry {
  4463  		return &projectCard, nil
  4464  	}
  4465  	path := fmt.Sprintf("/projects/columns/%d/cards", columnID)
  4466  	var retProjectCard ProjectCard
  4467  	_, err := c.request(&request{
  4468  		method:      http.MethodPost,
  4469  		path:        path,
  4470  		accept:      "application/vnd.github.inertia-preview+json",
  4471  		org:         org,
  4472  		requestBody: &projectCard,
  4473  		exitCodes:   []int{200},
  4474  	}, &retProjectCard)
  4475  	return &retProjectCard, err
  4476  }
  4477  
  4478  // GetProjectColumnCards get all project cards in a column. This helps in iterating all
  4479  // issues and PRs that are under a column
  4480  func (c *client) GetColumnProjectCards(org string, columnID int) ([]ProjectCard, error) {
  4481  	durationLogger := c.log("GetColumnProjectCards", columnID)
  4482  	defer durationLogger()
  4483  
  4484  	if c.fake {
  4485  		return nil, nil
  4486  	}
  4487  	path := fmt.Sprintf("/projects/columns/%d/cards", columnID)
  4488  	var cards []ProjectCard
  4489  	err := c.readPaginatedResults(
  4490  		path,
  4491  		// projects api requies the accept header to be set this way
  4492  		"application/vnd.github.inertia-preview+json",
  4493  		org,
  4494  		func() interface{} {
  4495  			return &[]ProjectCard{}
  4496  		},
  4497  		func(obj interface{}) {
  4498  			cards = append(cards, *(obj.(*[]ProjectCard))...)
  4499  		},
  4500  	)
  4501  	return cards, err
  4502  }
  4503  
  4504  // GetColumnProjectCard of a specific issue or PR for a specific column in a board/project
  4505  // This method requires the URL of the issue/pr to compare the issue with the content_url
  4506  // field of the card.  See https://developer.github.com/v3/projects/cards/#list-project-cards
  4507  func (c *client) GetColumnProjectCard(org string, columnID int, issueURL string) (*ProjectCard, error) {
  4508  	cards, err := c.GetColumnProjectCards(org, columnID)
  4509  	if err != nil {
  4510  		return nil, err
  4511  	}
  4512  
  4513  	for _, card := range cards {
  4514  		if card.ContentURL == issueURL {
  4515  			return &card, nil
  4516  		}
  4517  	}
  4518  	return nil, nil
  4519  }
  4520  
  4521  // MoveProjectCard moves a specific project card to a specified column in the same project
  4522  //
  4523  // See https://developer.github.com/v3/projects/cards/#move-a-project-card
  4524  func (c *client) MoveProjectCard(org string, projectCardID int, newColumnID int) error {
  4525  	durationLogger := c.log("MoveProjectCard", projectCardID, newColumnID)
  4526  	defer durationLogger()
  4527  
  4528  	reqParams := struct {
  4529  		Position string `json:"position"`
  4530  		ColumnID int    `json:"column_id"`
  4531  	}{"top", newColumnID}
  4532  
  4533  	_, err := c.request(&request{
  4534  		method:      http.MethodPost,
  4535  		path:        fmt.Sprintf("/projects/columns/cards/%d/moves", projectCardID),
  4536  		accept:      "application/vnd.github.inertia-preview+json",
  4537  		org:         org,
  4538  		requestBody: reqParams,
  4539  		exitCodes:   []int{201},
  4540  	}, nil)
  4541  	return err
  4542  }
  4543  
  4544  // DeleteProjectCard deletes the project card of a specific issue or PR
  4545  //
  4546  // See https://developer.github.com/v3/projects/cards/#delete-a-project-card
  4547  func (c *client) DeleteProjectCard(org string, projectCardID int) error {
  4548  	durationLogger := c.log("DeleteProjectCard", projectCardID)
  4549  	defer durationLogger()
  4550  
  4551  	_, err := c.request(&request{
  4552  		method:    http.MethodDelete,
  4553  		accept:    "application/vnd.github+json",
  4554  		path:      fmt.Sprintf("/projects/columns/cards/:%d", projectCardID),
  4555  		org:       org,
  4556  		exitCodes: []int{204},
  4557  	}, nil)
  4558  	return err
  4559  }
  4560  
  4561  // TeamHasMember checks if a user belongs to a team
  4562  // Deprecated: use TeamBySlugHasMember
  4563  func (c *client) TeamHasMember(org string, teamID int, memberLogin string) (bool, error) {
  4564  	durationLogger := c.log("TeamHasMember", teamID, memberLogin)
  4565  	defer durationLogger()
  4566  
  4567  	projectMaintainers, err := c.ListTeamMembers(org, teamID, RoleAll)
  4568  	if err != nil {
  4569  		return false, err
  4570  	}
  4571  	for _, person := range projectMaintainers {
  4572  		if NormLogin(person.Login) == NormLogin(memberLogin) {
  4573  			return true, nil
  4574  		}
  4575  	}
  4576  	return false, nil
  4577  }
  4578  
  4579  func (c *client) TeamBySlugHasMember(org string, teamSlug string, memberLogin string) (bool, error) {
  4580  	durationLogger := c.log("TeamBySlugHasMember", teamSlug, org)
  4581  	defer durationLogger()
  4582  
  4583  	if c.fake {
  4584  		return false, nil
  4585  	}
  4586  	exitCode, err := c.request(&request{
  4587  		method:    http.MethodGet,
  4588  		path:      fmt.Sprintf("/orgs/%s/teams/%s/memberships/%s", org, teamSlug, memberLogin),
  4589  		org:       org,
  4590  		exitCodes: []int{200, 404},
  4591  	}, nil)
  4592  	return exitCode == 200, err
  4593  }
  4594  
  4595  // GetTeamBySlug returns information about that team
  4596  //
  4597  // See https://developer.github.com/v3/teams/#get-team-by-name
  4598  func (c *client) GetTeamBySlug(slug string, org string) (*Team, error) {
  4599  	durationLogger := c.log("GetTeamBySlug", slug, org)
  4600  	defer durationLogger()
  4601  
  4602  	if c.fake {
  4603  		return &Team{}, nil
  4604  	}
  4605  	var team Team
  4606  	_, err := c.request(&request{
  4607  		method:    http.MethodGet,
  4608  		path:      fmt.Sprintf("/orgs/%s/teams/%s", org, slug),
  4609  		org:       org,
  4610  		exitCodes: []int{200},
  4611  	}, &team)
  4612  	if err != nil {
  4613  		return nil, err
  4614  	}
  4615  	return &team, err
  4616  }
  4617  
  4618  // ListCheckRuns lists all checkruns for the given ref
  4619  //
  4620  // See https://docs.github.com/en/rest/checks/runs#list-check-runs-for-a-git-reference
  4621  func (c *client) ListCheckRuns(org, repo, ref string) (*CheckRunList, error) {
  4622  	durationLogger := c.log("ListCheckRuns", org, repo, ref)
  4623  	defer durationLogger()
  4624  
  4625  	var checkRunList CheckRunList
  4626  	if err := c.readPaginatedResults(
  4627  		fmt.Sprintf("/repos/%s/%s/commits/%s/check-runs", org, repo, ref),
  4628  		"",
  4629  		org,
  4630  		func() interface{} {
  4631  			return &CheckRunList{}
  4632  		},
  4633  		func(obj interface{}) {
  4634  			cr := *(obj.(*CheckRunList))
  4635  			cr.CheckRuns = append(checkRunList.CheckRuns, cr.CheckRuns...)
  4636  			checkRunList = cr
  4637  		},
  4638  	); err != nil {
  4639  		return nil, err
  4640  	}
  4641  	return &checkRunList, nil
  4642  }
  4643  
  4644  // CreateCheckRun Creates a new check run for a specific commit in a repository.
  4645  //
  4646  // See https://docs.github.com/en/rest/checks/runs#create-a-check-run
  4647  func (c *client) CreateCheckRun(org, repo string, checkRun CheckRun) error {
  4648  	durationLogger := c.log("CreateCheckRun", org, repo, checkRun)
  4649  	defer durationLogger()
  4650  	_, err := c.request(&request{
  4651  		method:      http.MethodPost,
  4652  		path:        fmt.Sprintf("/repos/%s/%s/check-runs", org, repo),
  4653  		org:         org,
  4654  		requestBody: &checkRun,
  4655  		exitCodes:   []int{201},
  4656  	}, nil)
  4657  	if err != nil {
  4658  		return err
  4659  	}
  4660  	return nil
  4661  }
  4662  
  4663  // Simple function to check if GitHub App Authentication is being used
  4664  func (c *client) UsesAppAuth() bool {
  4665  	return c.delegate.usesAppsAuth
  4666  }
  4667  
  4668  // ListAppInstallations lists the installations for the current app. Will not work with
  4669  // a Personal Access Token.
  4670  //
  4671  // See https://docs.github.com/en/free-pro-team@latest/rest/reference/apps#list-installations-for-the-authenticated-app
  4672  func (c *client) ListAppInstallations() ([]AppInstallation, error) {
  4673  	durationLogger := c.log("AppInstallation")
  4674  	defer durationLogger()
  4675  
  4676  	var ais []AppInstallation
  4677  	if err := c.readPaginatedResults(
  4678  		"/app/installations",
  4679  		acceptNone,
  4680  		"",
  4681  		func() interface{} {
  4682  			return &[]AppInstallation{}
  4683  		},
  4684  		func(obj interface{}) {
  4685  			ais = append(ais, *(obj.(*[]AppInstallation))...)
  4686  		},
  4687  	); err != nil {
  4688  		return nil, err
  4689  	}
  4690  	return ais, nil
  4691  }
  4692  
  4693  // IsAppInstalled returns true if there is an app installation for the provided org and repo
  4694  // Will not work with a Personal Access Token.
  4695  //
  4696  // See https://docs.github.com/en/rest/apps/apps#get-a-repository-installation-for-the-authenticated-app
  4697  func (c *client) IsAppInstalled(org, repo string) (bool, error) {
  4698  	durationLogger := c.log("IsAppInstalled", org, repo)
  4699  	defer durationLogger()
  4700  
  4701  	if c.dry {
  4702  		return false, fmt.Errorf("not getting AppInstallation in dry-run mode")
  4703  	}
  4704  	if !c.usesAppsAuth {
  4705  		return false, fmt.Errorf("IsAppInstalled was called when not using appsAuth")
  4706  	}
  4707  
  4708  	code, err := c.request(&request{
  4709  		method:    http.MethodGet,
  4710  		path:      fmt.Sprintf("/repos/%s/%s/installation", org, repo),
  4711  		org:       org,
  4712  		exitCodes: []int{200, 404},
  4713  	}, nil)
  4714  	if err != nil {
  4715  		return false, err
  4716  	}
  4717  
  4718  	return code == 200, nil
  4719  }
  4720  
  4721  // ListAppInstallationsForOrg lists the installations for an organisation.
  4722  // The requestor must be  an organization owner with admin:read scope
  4723  //
  4724  // See https://docs.github.com/en/rest/orgs/orgs#list-app-installations-for-an-organization
  4725  func (c *client) ListAppInstallationsForOrg(org string) ([]AppInstallation, error) {
  4726  	durationLogger := c.log("AppInstallationForOrg")
  4727  	defer durationLogger()
  4728  
  4729  	var ais []AppInstallation
  4730  	if err := c.readPaginatedResults(
  4731  		fmt.Sprintf("/orgs/%s/installations", org),
  4732  		acceptNone,
  4733  		org,
  4734  		func() interface{} {
  4735  			return &AppInstallationList{}
  4736  		},
  4737  		func(obj interface{}) {
  4738  			ais = append(ais, obj.(*AppInstallationList).Installations...)
  4739  		},
  4740  	); err != nil {
  4741  		return nil, err
  4742  	}
  4743  	return ais, nil
  4744  }
  4745  
  4746  func (c *client) getAppInstallationToken(installationId int64) (*AppInstallationToken, error) {
  4747  	durationLogger := c.log("AppInstallationToken")
  4748  	defer durationLogger()
  4749  
  4750  	if c.dry {
  4751  		return nil, fmt.Errorf("not requesting GitHub App access_token in dry-run mode")
  4752  	}
  4753  
  4754  	var token AppInstallationToken
  4755  	if _, err := c.request(&request{
  4756  		method:    http.MethodPost,
  4757  		path:      fmt.Sprintf("/app/installations/%d/access_tokens", installationId),
  4758  		exitCodes: []int{201},
  4759  	}, &token); err != nil {
  4760  		return nil, err
  4761  	}
  4762  
  4763  	return &token, nil
  4764  }
  4765  
  4766  // GetApp gets the current app. Will not work with a Personal Access Token.
  4767  func (c *client) GetApp() (*App, error) {
  4768  	return c.GetAppWithContext(context.Background())
  4769  }
  4770  
  4771  func (c *client) GetAppWithContext(ctx context.Context) (*App, error) {
  4772  	durationLogger := c.log("App")
  4773  	defer durationLogger()
  4774  
  4775  	var app App
  4776  	if _, err := c.requestWithContext(ctx, &request{
  4777  		method:    http.MethodGet,
  4778  		path:      "/app",
  4779  		exitCodes: []int{200},
  4780  	}, &app); err != nil {
  4781  		return nil, err
  4782  	}
  4783  
  4784  	return &app, nil
  4785  }
  4786  
  4787  // GetDirectory uses GitHub repo contents API to retrieve the content of a directory with commit SHA.
  4788  // If commit is empty, it will grab content from repo's default branch, usually master.
  4789  //
  4790  // See https://developer.github.com/v3/repos/contents/#get-contents
  4791  func (c *client) GetDirectory(org, repo, dirpath, commit string) ([]DirectoryContent, error) {
  4792  	durationLogger := c.log("GetDirectory", org, repo, dirpath, commit)
  4793  	defer durationLogger()
  4794  
  4795  	path := fmt.Sprintf("/repos/%s/%s/contents/%s", org, repo, dirpath)
  4796  	if commit != "" {
  4797  		path = fmt.Sprintf("%s?ref=%s", path, url.QueryEscape(commit))
  4798  	}
  4799  
  4800  	var res []DirectoryContent
  4801  	code, err := c.request(&request{
  4802  		method:    http.MethodGet,
  4803  		path:      path,
  4804  		org:       org,
  4805  		exitCodes: []int{200, 404},
  4806  	}, &res)
  4807  
  4808  	if err != nil {
  4809  		return nil, err
  4810  	}
  4811  
  4812  	if code == 404 {
  4813  		return nil, &FileNotFound{
  4814  			org:    org,
  4815  			repo:   repo,
  4816  			path:   dirpath,
  4817  			commit: commit,
  4818  		}
  4819  	}
  4820  
  4821  	return res, nil
  4822  }
  4823  
  4824  // CreatePullRequestReviewComment creates a review comment on a PR.
  4825  //
  4826  // See also: https://docs.github.com/en/rest/reference/pulls#create-a-review-comment-for-a-pull-request
  4827  func (c *client) CreatePullRequestReviewComment(org, repo string, number int, rc ReviewComment) error {
  4828  	c.log("CreatePullRequestReviewComment", org, repo, number, rc)
  4829  
  4830  	// TODO: remove custom Accept headers when their respective API fully launches.
  4831  	acceptHeaders := []string{
  4832  		// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
  4833  		"application/vnd.github.squirrel-girl-preview",
  4834  		// https://developer.github.com/changes/2019-10-03-multi-line-comments/
  4835  		"application/vnd.github.comfort-fade-preview+json",
  4836  	}
  4837  
  4838  	_, err := c.request(&request{
  4839  		method:      http.MethodPost,
  4840  		accept:      strings.Join(acceptHeaders, ", "),
  4841  		path:        fmt.Sprintf("/repos/%s/%s/pulls/%d/comments", org, repo, number),
  4842  		org:         org,
  4843  		requestBody: &rc,
  4844  		exitCodes:   []int{201},
  4845  	}, nil)
  4846  	return err
  4847  }