github.com/google/go-github/v60@v60.0.0/github/github.go (about) 1 // Copyright 2013 The go-github AUTHORS. All rights reserved. 2 // 3 // Use of this source code is governed by a BSD-style 4 // license that can be found in the LICENSE file. 5 6 //go:generate go run gen-accessors.go 7 //go:generate go run gen-stringify-test.go 8 //go:generate ../script/metadata.sh update-go 9 10 package github 11 12 import ( 13 "bytes" 14 "context" 15 "encoding/json" 16 "errors" 17 "fmt" 18 "io" 19 "net/http" 20 "net/url" 21 "reflect" 22 "strconv" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/google/go-querystring/query" 28 ) 29 30 const ( 31 Version = "v60.0.0" 32 33 defaultAPIVersion = "2022-11-28" 34 defaultBaseURL = "https://api.github.com/" 35 defaultUserAgent = "go-github" + "/" + Version 36 uploadBaseURL = "https://uploads.github.com/" 37 38 headerAPIVersion = "X-GitHub-Api-Version" 39 headerRateLimit = "X-RateLimit-Limit" 40 headerRateRemaining = "X-RateLimit-Remaining" 41 headerRateReset = "X-RateLimit-Reset" 42 headerOTP = "X-GitHub-OTP" 43 headerRetryAfter = "Retry-After" 44 45 headerTokenExpiration = "GitHub-Authentication-Token-Expiration" 46 47 mediaTypeV3 = "application/vnd.github.v3+json" 48 defaultMediaType = "application/octet-stream" 49 mediaTypeV3SHA = "application/vnd.github.v3.sha" 50 mediaTypeV3Diff = "application/vnd.github.v3.diff" 51 mediaTypeV3Patch = "application/vnd.github.v3.patch" 52 mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json" 53 mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json" 54 55 // Media Type values to access preview APIs 56 // These media types will be added to the API request as headers 57 // and used to enable particular features on GitHub API that are still in preview. 58 // After some time, specific media types will be promoted (to a "stable" state). 59 // From then on, the preview headers are not required anymore to activate the additional 60 // feature on GitHub.com's API. However, this API header might still be needed for users 61 // to run a GitHub Enterprise Server on-premise. 62 // It's not uncommon for GitHub Enterprise Server customers to run older versions which 63 // would probably rely on the preview headers for some time. 64 // While the header promotion is going out for GitHub.com, it may be some time before it 65 // even arrives in GitHub Enterprise Server. 66 // We keep those preview headers around to avoid breaking older GitHub Enterprise Server 67 // versions. Additionally, non-functional (preview) headers don't create any side effects 68 // on GitHub Cloud version. 69 // 70 // See https://github.com/google/go-github/pull/2125 for full context. 71 72 // https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/ 73 mediaTypeStarringPreview = "application/vnd.github.v3.star+json" 74 75 // https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/ 76 mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json" 77 78 // https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/ 79 mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json" 80 81 // https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/ 82 mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json" 83 84 // https://developer.github.com/changes/2016-05-12-reactions-api-preview/ 85 mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview" 86 87 // https://developer.github.com/changes/2016-05-23-timeline-preview-api/ 88 mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json" 89 90 // https://developer.github.com/changes/2016-09-14-projects-api/ 91 mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json" 92 93 // https://developer.github.com/changes/2017-01-05-commit-search-api/ 94 mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json" 95 96 // https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/ 97 mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json" 98 99 // https://developer.github.com/changes/2017-05-23-coc-api/ 100 mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json" 101 102 // https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/ 103 mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json" 104 105 // https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/ 106 mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json" 107 108 // https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/ 109 mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json" 110 111 // https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/ 112 mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview" 113 114 // https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/ 115 mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json" 116 117 // https://developer.github.com/changes/2018-09-05-project-card-events/ 118 mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json" 119 120 // https://developer.github.com/changes/2018-12-18-interactions-preview/ 121 mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json" 122 123 // https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/ 124 mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json" 125 126 // https://developer.github.com/changes/2019-04-24-vulnerability-alerts/ 127 mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json" 128 129 // https://developer.github.com/changes/2019-05-29-update-branch-api/ 130 mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json" 131 132 // https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/ 133 mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json" 134 135 // https://docs.github.com/rest/previews/#repository-creation-permissions 136 mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json" 137 138 // https://docs.github.com/rest/previews/#create-and-use-repository-templates 139 mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json" 140 141 // https://developer.github.com/changes/2019-10-03-multi-line-comments/ 142 mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json" 143 144 // https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/ 145 mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json" 146 147 // https://developer.github.com/changes/2019-12-03-internal-visibility-changes/ 148 mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json" 149 150 // https://developer.github.com/changes/2018-12-10-content-attachments-api/ 151 mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json" 152 ) 153 154 var errNonNilContext = errors.New("context must be non-nil") 155 156 // A Client manages communication with the GitHub API. 157 type Client struct { 158 clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. 159 client *http.Client // HTTP client used to communicate with the API. 160 161 // Base URL for API requests. Defaults to the public GitHub API, but can be 162 // set to a domain endpoint to use with GitHub Enterprise. BaseURL should 163 // always be specified with a trailing slash. 164 BaseURL *url.URL 165 166 // Base URL for uploading files. 167 UploadURL *url.URL 168 169 // User agent used when communicating with the GitHub API. 170 UserAgent string 171 172 rateMu sync.Mutex 173 rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls. 174 secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls. 175 176 common service // Reuse a single struct instead of allocating one for each service on the heap. 177 178 // Services used for talking to different parts of the GitHub API. 179 Actions *ActionsService 180 Activity *ActivityService 181 Admin *AdminService 182 Apps *AppsService 183 Authorizations *AuthorizationsService 184 Billing *BillingService 185 Checks *ChecksService 186 CodeScanning *CodeScanningService 187 CodesOfConduct *CodesOfConductService 188 Codespaces *CodespacesService 189 Copilot *CopilotService 190 Dependabot *DependabotService 191 DependencyGraph *DependencyGraphService 192 Emojis *EmojisService 193 Enterprise *EnterpriseService 194 Gists *GistsService 195 Git *GitService 196 Gitignores *GitignoresService 197 Interactions *InteractionsService 198 IssueImport *IssueImportService 199 Issues *IssuesService 200 Licenses *LicensesService 201 Markdown *MarkdownService 202 Marketplace *MarketplaceService 203 Meta *MetaService 204 Migrations *MigrationService 205 Organizations *OrganizationsService 206 Projects *ProjectsService 207 PullRequests *PullRequestsService 208 RateLimit *RateLimitService 209 Reactions *ReactionsService 210 Repositories *RepositoriesService 211 SCIM *SCIMService 212 Search *SearchService 213 SecretScanning *SecretScanningService 214 SecurityAdvisories *SecurityAdvisoriesService 215 Teams *TeamsService 216 Users *UsersService 217 } 218 219 type service struct { 220 client *Client 221 } 222 223 // Client returns the http.Client used by this GitHub client. 224 // This should only be used for requests to the GitHub API because 225 // request headers will contain an authorization token. 226 func (c *Client) Client() *http.Client { 227 c.clientMu.Lock() 228 defer c.clientMu.Unlock() 229 clientCopy := *c.client 230 return &clientCopy 231 } 232 233 // ListOptions specifies the optional parameters to various List methods that 234 // support offset pagination. 235 type ListOptions struct { 236 // For paginated result sets, page of results to retrieve. 237 Page int `url:"page,omitempty"` 238 239 // For paginated result sets, the number of results to include per page. 240 PerPage int `url:"per_page,omitempty"` 241 } 242 243 // ListCursorOptions specifies the optional parameters to various List methods that 244 // support cursor pagination. 245 type ListCursorOptions struct { 246 // For paginated result sets, page of results to retrieve. 247 Page string `url:"page,omitempty"` 248 249 // For paginated result sets, the number of results to include per page. 250 PerPage int `url:"per_page,omitempty"` 251 252 // For paginated result sets, the number of results per page (max 100), starting from the first matching result. 253 // This parameter must not be used in combination with last. 254 First int `url:"first,omitempty"` 255 256 // For paginated result sets, the number of results per page (max 100), starting from the last matching result. 257 // This parameter must not be used in combination with first. 258 Last int `url:"last,omitempty"` 259 260 // A cursor, as given in the Link header. If specified, the query only searches for events after this cursor. 261 After string `url:"after,omitempty"` 262 263 // A cursor, as given in the Link header. If specified, the query only searches for events before this cursor. 264 Before string `url:"before,omitempty"` 265 266 // A cursor, as given in the Link header. If specified, the query continues the search using this cursor. 267 Cursor string `url:"cursor,omitempty"` 268 } 269 270 // UploadOptions specifies the parameters to methods that support uploads. 271 type UploadOptions struct { 272 Name string `url:"name,omitempty"` 273 Label string `url:"label,omitempty"` 274 MediaType string `url:"-"` 275 } 276 277 // RawType represents type of raw format of a request instead of JSON. 278 type RawType uint8 279 280 const ( 281 // Diff format. 282 Diff RawType = 1 + iota 283 // Patch format. 284 Patch 285 ) 286 287 // RawOptions specifies parameters when user wants to get raw format of 288 // a response instead of JSON. 289 type RawOptions struct { 290 Type RawType 291 } 292 293 // addOptions adds the parameters in opts as URL query parameters to s. opts 294 // must be a struct whose fields may contain "url" tags. 295 func addOptions(s string, opts interface{}) (string, error) { 296 v := reflect.ValueOf(opts) 297 if v.Kind() == reflect.Ptr && v.IsNil() { 298 return s, nil 299 } 300 301 u, err := url.Parse(s) 302 if err != nil { 303 return s, err 304 } 305 306 qs, err := query.Values(opts) 307 if err != nil { 308 return s, err 309 } 310 311 u.RawQuery = qs.Encode() 312 return u.String(), nil 313 } 314 315 // NewClient returns a new GitHub API client. If a nil httpClient is 316 // provided, a new http.Client will be used. To use API methods which require 317 // authentication, either use Client.WithAuthToken or provide NewClient with 318 // an http.Client that will perform the authentication for you (such as that 319 // provided by the golang.org/x/oauth2 library). 320 func NewClient(httpClient *http.Client) *Client { 321 if httpClient == nil { 322 httpClient = &http.Client{} 323 } 324 httpClient2 := *httpClient 325 c := &Client{client: &httpClient2} 326 c.initialize() 327 return c 328 } 329 330 // WithAuthToken returns a copy of the client configured to use the provided token for the Authorization header. 331 func (c *Client) WithAuthToken(token string) *Client { 332 c2 := c.copy() 333 defer c2.initialize() 334 transport := c2.client.Transport 335 if transport == nil { 336 transport = http.DefaultTransport 337 } 338 c2.client.Transport = roundTripperFunc( 339 func(req *http.Request) (*http.Response, error) { 340 req = req.Clone(req.Context()) 341 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 342 return transport.RoundTrip(req) 343 }, 344 ) 345 return c2 346 } 347 348 // WithEnterpriseURLs returns a copy of the client configured to use the provided base and 349 // upload URLs. If the base URL does not have the suffix "/api/v3/", it will be added 350 // automatically. If the upload URL does not have the suffix "/api/uploads", it will be 351 // added automatically. 352 // 353 // Note that WithEnterpriseURLs is a convenience helper only; 354 // its behavior is equivalent to setting the BaseURL and UploadURL fields. 355 // 356 // Another important thing is that by default, the GitHub Enterprise URL format 357 // should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code. 358 // The upload URL format should be http(s)://[hostname]/api/uploads/. 359 func (c *Client) WithEnterpriseURLs(baseURL, uploadURL string) (*Client, error) { 360 c2 := c.copy() 361 defer c2.initialize() 362 var err error 363 c2.BaseURL, err = url.Parse(baseURL) 364 if err != nil { 365 return nil, err 366 } 367 368 if !strings.HasSuffix(c2.BaseURL.Path, "/") { 369 c2.BaseURL.Path += "/" 370 } 371 if !strings.HasSuffix(c2.BaseURL.Path, "/api/v3/") && 372 !strings.HasPrefix(c2.BaseURL.Host, "api.") && 373 !strings.Contains(c2.BaseURL.Host, ".api.") { 374 c2.BaseURL.Path += "api/v3/" 375 } 376 377 c2.UploadURL, err = url.Parse(uploadURL) 378 if err != nil { 379 return nil, err 380 } 381 382 if !strings.HasSuffix(c2.UploadURL.Path, "/") { 383 c2.UploadURL.Path += "/" 384 } 385 if !strings.HasSuffix(c2.UploadURL.Path, "/api/uploads/") && 386 !strings.HasPrefix(c2.UploadURL.Host, "api.") && 387 !strings.Contains(c2.UploadURL.Host, ".api.") { 388 c2.UploadURL.Path += "api/uploads/" 389 } 390 return c2, nil 391 } 392 393 // initialize sets default values and initializes services. 394 func (c *Client) initialize() { 395 if c.client == nil { 396 c.client = &http.Client{} 397 } 398 if c.BaseURL == nil { 399 c.BaseURL, _ = url.Parse(defaultBaseURL) 400 } 401 if c.UploadURL == nil { 402 c.UploadURL, _ = url.Parse(uploadBaseURL) 403 } 404 if c.UserAgent == "" { 405 c.UserAgent = defaultUserAgent 406 } 407 c.common.client = c 408 c.Actions = (*ActionsService)(&c.common) 409 c.Activity = (*ActivityService)(&c.common) 410 c.Admin = (*AdminService)(&c.common) 411 c.Apps = (*AppsService)(&c.common) 412 c.Authorizations = (*AuthorizationsService)(&c.common) 413 c.Billing = (*BillingService)(&c.common) 414 c.Checks = (*ChecksService)(&c.common) 415 c.CodeScanning = (*CodeScanningService)(&c.common) 416 c.Codespaces = (*CodespacesService)(&c.common) 417 c.CodesOfConduct = (*CodesOfConductService)(&c.common) 418 c.Copilot = (*CopilotService)(&c.common) 419 c.Dependabot = (*DependabotService)(&c.common) 420 c.DependencyGraph = (*DependencyGraphService)(&c.common) 421 c.Emojis = (*EmojisService)(&c.common) 422 c.Enterprise = (*EnterpriseService)(&c.common) 423 c.Gists = (*GistsService)(&c.common) 424 c.Git = (*GitService)(&c.common) 425 c.Gitignores = (*GitignoresService)(&c.common) 426 c.Interactions = (*InteractionsService)(&c.common) 427 c.IssueImport = (*IssueImportService)(&c.common) 428 c.Issues = (*IssuesService)(&c.common) 429 c.Licenses = (*LicensesService)(&c.common) 430 c.Markdown = (*MarkdownService)(&c.common) 431 c.Marketplace = &MarketplaceService{client: c} 432 c.Meta = (*MetaService)(&c.common) 433 c.Migrations = (*MigrationService)(&c.common) 434 c.Organizations = (*OrganizationsService)(&c.common) 435 c.Projects = (*ProjectsService)(&c.common) 436 c.PullRequests = (*PullRequestsService)(&c.common) 437 c.RateLimit = (*RateLimitService)(&c.common) 438 c.Reactions = (*ReactionsService)(&c.common) 439 c.Repositories = (*RepositoriesService)(&c.common) 440 c.SCIM = (*SCIMService)(&c.common) 441 c.Search = (*SearchService)(&c.common) 442 c.SecretScanning = (*SecretScanningService)(&c.common) 443 c.SecurityAdvisories = (*SecurityAdvisoriesService)(&c.common) 444 c.Teams = (*TeamsService)(&c.common) 445 c.Users = (*UsersService)(&c.common) 446 } 447 448 // copy returns a copy of the current client. It must be initialized before use. 449 func (c *Client) copy() *Client { 450 c.clientMu.Lock() 451 // can't use *c here because that would copy mutexes by value. 452 clone := Client{ 453 client: &http.Client{}, 454 UserAgent: c.UserAgent, 455 BaseURL: c.BaseURL, 456 UploadURL: c.UploadURL, 457 secondaryRateLimitReset: c.secondaryRateLimitReset, 458 } 459 c.clientMu.Unlock() 460 if c.client != nil { 461 clone.client.Transport = c.client.Transport 462 clone.client.CheckRedirect = c.client.CheckRedirect 463 clone.client.Jar = c.client.Jar 464 clone.client.Timeout = c.client.Timeout 465 } 466 c.rateMu.Lock() 467 copy(clone.rateLimits[:], c.rateLimits[:]) 468 c.rateMu.Unlock() 469 return &clone 470 } 471 472 // NewClientWithEnvProxy enhances NewClient with the HttpProxy env. 473 func NewClientWithEnvProxy() *Client { 474 return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}}) 475 } 476 477 // NewTokenClient returns a new GitHub API client authenticated with the provided token. 478 // Deprecated: Use NewClient(nil).WithAuthToken(token) instead. 479 func NewTokenClient(_ context.Context, token string) *Client { 480 // This always returns a nil error. 481 return NewClient(nil).WithAuthToken(token) 482 } 483 484 // NewEnterpriseClient returns a new GitHub API client with provided 485 // base URL and upload URL (often is your GitHub Enterprise hostname). 486 // 487 // Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead. 488 func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) { 489 return NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) 490 } 491 492 // RequestOption represents an option that can modify an http.Request. 493 type RequestOption func(req *http.Request) 494 495 // WithVersion overrides the GitHub v3 API version for this individual request. 496 // For more information, see: 497 // https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/ 498 func WithVersion(version string) RequestOption { 499 return func(req *http.Request) { 500 req.Header.Set(headerAPIVersion, version) 501 } 502 } 503 504 // NewRequest creates an API request. A relative URL can be provided in urlStr, 505 // in which case it is resolved relative to the BaseURL of the Client. 506 // Relative URLs should always be specified without a preceding slash. If 507 // specified, the value pointed to by body is JSON encoded and included as the 508 // request body. 509 func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) { 510 if !strings.HasSuffix(c.BaseURL.Path, "/") { 511 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) 512 } 513 514 u, err := c.BaseURL.Parse(urlStr) 515 if err != nil { 516 return nil, err 517 } 518 519 var buf io.ReadWriter 520 if body != nil { 521 buf = &bytes.Buffer{} 522 enc := json.NewEncoder(buf) 523 enc.SetEscapeHTML(false) 524 err := enc.Encode(body) 525 if err != nil { 526 return nil, err 527 } 528 } 529 530 req, err := http.NewRequest(method, u.String(), buf) 531 if err != nil { 532 return nil, err 533 } 534 535 if body != nil { 536 req.Header.Set("Content-Type", "application/json") 537 } 538 req.Header.Set("Accept", mediaTypeV3) 539 if c.UserAgent != "" { 540 req.Header.Set("User-Agent", c.UserAgent) 541 } 542 req.Header.Set(headerAPIVersion, defaultAPIVersion) 543 544 for _, opt := range opts { 545 opt(req) 546 } 547 548 return req, nil 549 } 550 551 // NewFormRequest creates an API request. A relative URL can be provided in urlStr, 552 // in which case it is resolved relative to the BaseURL of the Client. 553 // Relative URLs should always be specified without a preceding slash. 554 // Body is sent with Content-Type: application/x-www-form-urlencoded. 555 func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) { 556 if !strings.HasSuffix(c.BaseURL.Path, "/") { 557 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) 558 } 559 560 u, err := c.BaseURL.Parse(urlStr) 561 if err != nil { 562 return nil, err 563 } 564 565 req, err := http.NewRequest(http.MethodPost, u.String(), body) 566 if err != nil { 567 return nil, err 568 } 569 570 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 571 req.Header.Set("Accept", mediaTypeV3) 572 if c.UserAgent != "" { 573 req.Header.Set("User-Agent", c.UserAgent) 574 } 575 req.Header.Set(headerAPIVersion, defaultAPIVersion) 576 577 for _, opt := range opts { 578 opt(req) 579 } 580 581 return req, nil 582 } 583 584 // NewUploadRequest creates an upload request. A relative URL can be provided in 585 // urlStr, in which case it is resolved relative to the UploadURL of the Client. 586 // Relative URLs should always be specified without a preceding slash. 587 func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) { 588 if !strings.HasSuffix(c.UploadURL.Path, "/") { 589 return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL) 590 } 591 u, err := c.UploadURL.Parse(urlStr) 592 if err != nil { 593 return nil, err 594 } 595 596 req, err := http.NewRequest("POST", u.String(), reader) 597 if err != nil { 598 return nil, err 599 } 600 601 req.ContentLength = size 602 603 if mediaType == "" { 604 mediaType = defaultMediaType 605 } 606 req.Header.Set("Content-Type", mediaType) 607 req.Header.Set("Accept", mediaTypeV3) 608 req.Header.Set("User-Agent", c.UserAgent) 609 req.Header.Set(headerAPIVersion, defaultAPIVersion) 610 611 for _, opt := range opts { 612 opt(req) 613 } 614 615 return req, nil 616 } 617 618 // Response is a GitHub API response. This wraps the standard http.Response 619 // returned from GitHub and provides convenient access to things like 620 // pagination links. 621 type Response struct { 622 *http.Response 623 624 // These fields provide the page values for paginating through a set of 625 // results. Any or all of these may be set to the zero value for 626 // responses that are not part of a paginated set, or for which there 627 // are no additional pages. 628 // 629 // These fields support what is called "offset pagination" and should 630 // be used with the ListOptions struct. 631 NextPage int 632 PrevPage int 633 FirstPage int 634 LastPage int 635 636 // Additionally, some APIs support "cursor pagination" instead of offset. 637 // This means that a token points directly to the next record which 638 // can lead to O(1) performance compared to O(n) performance provided 639 // by offset pagination. 640 // 641 // For APIs that support cursor pagination (such as 642 // TeamsService.ListIDPGroupsInOrganization), the following field 643 // will be populated to point to the next page. 644 // 645 // To use this token, set ListCursorOptions.Page to this value before 646 // calling the endpoint again. 647 NextPageToken string 648 649 // For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries, 650 // the following field will be populated to point to the next page. 651 // Set ListCursorOptions.Cursor to this value when calling the endpoint again. 652 Cursor string 653 654 // For APIs that support before/after pagination, such as OrganizationsService.AuditLog. 655 Before string 656 After string 657 658 // Explicitly specify the Rate type so Rate's String() receiver doesn't 659 // propagate to Response. 660 Rate Rate 661 662 // token's expiration date. Timestamp is 0001-01-01 when token doesn't expire. 663 // So it is valid for TokenExpiration.Equal(Timestamp{}) or TokenExpiration.Time.After(time.Now()) 664 TokenExpiration Timestamp 665 } 666 667 // newResponse creates a new Response for the provided http.Response. 668 // r must not be nil. 669 func newResponse(r *http.Response) *Response { 670 response := &Response{Response: r} 671 response.populatePageValues() 672 response.Rate = parseRate(r) 673 response.TokenExpiration = parseTokenExpiration(r) 674 return response 675 } 676 677 // populatePageValues parses the HTTP Link response headers and populates the 678 // various pagination link values in the Response. 679 func (r *Response) populatePageValues() { 680 if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { 681 for _, link := range strings.Split(links[0], ",") { 682 segments := strings.Split(strings.TrimSpace(link), ";") 683 684 // link must at least have href and rel 685 if len(segments) < 2 { 686 continue 687 } 688 689 // ensure href is properly formatted 690 if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { 691 continue 692 } 693 694 // try to pull out page parameter 695 url, err := url.Parse(segments[0][1 : len(segments[0])-1]) 696 if err != nil { 697 continue 698 } 699 700 q := url.Query() 701 702 if cursor := q.Get("cursor"); cursor != "" { 703 for _, segment := range segments[1:] { 704 switch strings.TrimSpace(segment) { 705 case `rel="next"`: 706 r.Cursor = cursor 707 } 708 } 709 710 continue 711 } 712 713 page := q.Get("page") 714 since := q.Get("since") 715 before := q.Get("before") 716 after := q.Get("after") 717 718 if page == "" && before == "" && after == "" && since == "" { 719 continue 720 } 721 722 if since != "" && page == "" { 723 page = since 724 } 725 726 for _, segment := range segments[1:] { 727 switch strings.TrimSpace(segment) { 728 case `rel="next"`: 729 if r.NextPage, err = strconv.Atoi(page); err != nil { 730 r.NextPageToken = page 731 } 732 r.After = after 733 case `rel="prev"`: 734 r.PrevPage, _ = strconv.Atoi(page) 735 r.Before = before 736 case `rel="first"`: 737 r.FirstPage, _ = strconv.Atoi(page) 738 case `rel="last"`: 739 r.LastPage, _ = strconv.Atoi(page) 740 } 741 } 742 } 743 } 744 } 745 746 // parseRate parses the rate related headers. 747 func parseRate(r *http.Response) Rate { 748 var rate Rate 749 if limit := r.Header.Get(headerRateLimit); limit != "" { 750 rate.Limit, _ = strconv.Atoi(limit) 751 } 752 if remaining := r.Header.Get(headerRateRemaining); remaining != "" { 753 rate.Remaining, _ = strconv.Atoi(remaining) 754 } 755 if reset := r.Header.Get(headerRateReset); reset != "" { 756 if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { 757 rate.Reset = Timestamp{time.Unix(v, 0)} 758 } 759 } 760 return rate 761 } 762 763 // parseSecondaryRate parses the secondary rate related headers, 764 // and returns the time to retry after. 765 func parseSecondaryRate(r *http.Response) *time.Duration { 766 // According to GitHub support, the "Retry-After" header value will be 767 // an integer which represents the number of seconds that one should 768 // wait before resuming making requests. 769 if v := r.Header.Get(headerRetryAfter); v != "" { 770 retryAfterSeconds, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. 771 retryAfter := time.Duration(retryAfterSeconds) * time.Second 772 return &retryAfter 773 } 774 775 // According to GitHub support, endpoints might return x-ratelimit-reset instead, 776 // as an integer which represents the number of seconds since epoch UTC, 777 // represting the time to resume making requests. 778 if v := r.Header.Get(headerRateReset); v != "" { 779 secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop. 780 retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0)) 781 return &retryAfter 782 } 783 784 return nil 785 } 786 787 // parseTokenExpiration parses the TokenExpiration related headers. 788 // Returns 0001-01-01 if the header is not defined or could not be parsed. 789 func parseTokenExpiration(r *http.Response) Timestamp { 790 if v := r.Header.Get(headerTokenExpiration); v != "" { 791 if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil { 792 return Timestamp{t.Local()} 793 } 794 // Some tokens include the timezone offset instead of the timezone. 795 // https://github.com/google/go-github/issues/2649 796 if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil { 797 return Timestamp{t.Local()} 798 } 799 } 800 return Timestamp{} // 0001-01-01 00:00:00 801 } 802 803 type requestContext uint8 804 805 const ( 806 bypassRateLimitCheck requestContext = iota 807 ) 808 809 // BareDo sends an API request and lets you handle the api response. If an error 810 // or API Error occurs, the error will contain more information. Otherwise you 811 // are supposed to read and close the response's Body. If rate limit is exceeded 812 // and reset time is in the future, BareDo returns *RateLimitError immediately 813 // without making a network API call. 814 // 815 // The provided ctx must be non-nil, if it is nil an error is returned. If it is 816 // canceled or times out, ctx.Err() will be returned. 817 func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) { 818 if ctx == nil { 819 return nil, errNonNilContext 820 } 821 822 req = withContext(ctx, req) 823 824 rateLimitCategory := category(req.Method, req.URL.Path) 825 826 if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil { 827 // If we've hit rate limit, don't make further requests before Reset time. 828 if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil { 829 return &Response{ 830 Response: err.Response, 831 Rate: err.Rate, 832 }, err 833 } 834 // If we've hit a secondary rate limit, don't make further requests before Retry After. 835 if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil { 836 return &Response{ 837 Response: err.Response, 838 }, err 839 } 840 } 841 842 resp, err := c.client.Do(req) 843 if err != nil { 844 // If we got an error, and the context has been canceled, 845 // the context's error is probably more useful. 846 select { 847 case <-ctx.Done(): 848 return nil, ctx.Err() 849 default: 850 } 851 852 // If the error type is *url.Error, sanitize its URL before returning. 853 if e, ok := err.(*url.Error); ok { 854 if url, err := url.Parse(e.URL); err == nil { 855 e.URL = sanitizeURL(url).String() 856 return nil, e 857 } 858 } 859 860 return nil, err 861 } 862 863 response := newResponse(resp) 864 865 // Don't update the rate limits if this was a cached response. 866 // X-From-Cache is set by https://github.com/gregjones/httpcache 867 if response.Header.Get("X-From-Cache") == "" { 868 c.rateMu.Lock() 869 c.rateLimits[rateLimitCategory] = response.Rate 870 c.rateMu.Unlock() 871 } 872 873 err = CheckResponse(resp) 874 if err != nil { 875 defer resp.Body.Close() 876 // Special case for AcceptedErrors. If an AcceptedError 877 // has been encountered, the response's payload will be 878 // added to the AcceptedError and returned. 879 // 880 // Issue #1022 881 aerr, ok := err.(*AcceptedError) 882 if ok { 883 b, readErr := io.ReadAll(resp.Body) 884 if readErr != nil { 885 return response, readErr 886 } 887 888 aerr.Raw = b 889 err = aerr 890 } 891 892 // Update the secondary rate limit if we hit it. 893 rerr, ok := err.(*AbuseRateLimitError) 894 if ok && rerr.RetryAfter != nil { 895 c.rateMu.Lock() 896 c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter) 897 c.rateMu.Unlock() 898 } 899 } 900 return response, err 901 } 902 903 // Do sends an API request and returns the API response. The API response is 904 // JSON decoded and stored in the value pointed to by v, or returned as an 905 // error if an API error has occurred. If v implements the io.Writer interface, 906 // the raw response body will be written to v, without attempting to first 907 // decode it. If v is nil, and no error happens, the response is returned as is. 908 // If rate limit is exceeded and reset time is in the future, Do returns 909 // *RateLimitError immediately without making a network API call. 910 // 911 // The provided ctx must be non-nil, if it is nil an error is returned. If it 912 // is canceled or times out, ctx.Err() will be returned. 913 func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { 914 resp, err := c.BareDo(ctx, req) 915 if err != nil { 916 return resp, err 917 } 918 defer resp.Body.Close() 919 920 switch v := v.(type) { 921 case nil: 922 case io.Writer: 923 _, err = io.Copy(v, resp.Body) 924 default: 925 decErr := json.NewDecoder(resp.Body).Decode(v) 926 if decErr == io.EOF { 927 decErr = nil // ignore EOF errors caused by empty response body 928 } 929 if decErr != nil { 930 err = decErr 931 } 932 } 933 return resp, err 934 } 935 936 // checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 937 // current client state in order to quickly check if *RateLimitError can be immediately returned 938 // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 939 // Otherwise it returns nil, and Client.Do should proceed normally. 940 func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError { 941 c.rateMu.Lock() 942 rate := c.rateLimits[rateLimitCategory] 943 c.rateMu.Unlock() 944 if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) { 945 // Create a fake response. 946 resp := &http.Response{ 947 Status: http.StatusText(http.StatusForbidden), 948 StatusCode: http.StatusForbidden, 949 Request: req, 950 Header: make(http.Header), 951 Body: io.NopCloser(strings.NewReader("")), 952 } 953 return &RateLimitError{ 954 Rate: rate, 955 Response: resp, 956 Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time), 957 } 958 } 959 960 return nil 961 } 962 963 // checkSecondaryRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 964 // current client state in order to quickly check if *AbuseRateLimitError can be immediately returned 965 // from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 966 // Otherwise it returns nil, and Client.Do should proceed normally. 967 func (c *Client) checkSecondaryRateLimitBeforeDo(req *http.Request) *AbuseRateLimitError { 968 c.rateMu.Lock() 969 secondary := c.secondaryRateLimitReset 970 c.rateMu.Unlock() 971 if !secondary.IsZero() && time.Now().Before(secondary) { 972 // Create a fake response. 973 resp := &http.Response{ 974 Status: http.StatusText(http.StatusForbidden), 975 StatusCode: http.StatusForbidden, 976 Request: req, 977 Header: make(http.Header), 978 Body: io.NopCloser(strings.NewReader("")), 979 } 980 981 retryAfter := time.Until(secondary) 982 return &AbuseRateLimitError{ 983 Response: resp, 984 Message: fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary), 985 RetryAfter: &retryAfter, 986 } 987 } 988 989 return nil 990 } 991 992 // compareHTTPResponse returns whether two http.Response objects are equal or not. 993 // Currently, only StatusCode is checked. This function is used when implementing the 994 // Is(error) bool interface for the custom error types in this package. 995 func compareHTTPResponse(r1, r2 *http.Response) bool { 996 if r1 == nil && r2 == nil { 997 return true 998 } 999 1000 if r1 != nil && r2 != nil { 1001 return r1.StatusCode == r2.StatusCode 1002 } 1003 return false 1004 } 1005 1006 /* 1007 An ErrorResponse reports one or more errors caused by an API request. 1008 1009 GitHub API docs: https://docs.github.com/rest/#client-errors 1010 */ 1011 type ErrorResponse struct { 1012 Response *http.Response `json:"-"` // HTTP response that caused this error 1013 Message string `json:"message"` // error message 1014 Errors []Error `json:"errors"` // more detail on individual errors 1015 // Block is only populated on certain types of errors such as code 451. 1016 Block *ErrorBlock `json:"block,omitempty"` 1017 // Most errors will also include a documentation_url field pointing 1018 // to some content that might help you resolve the error, see 1019 // https://docs.github.com/rest/#client-errors 1020 DocumentationURL string `json:"documentation_url,omitempty"` 1021 } 1022 1023 // ErrorBlock contains a further explanation for the reason of an error. 1024 // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/ 1025 // for more information. 1026 type ErrorBlock struct { 1027 Reason string `json:"reason,omitempty"` 1028 CreatedAt *Timestamp `json:"created_at,omitempty"` 1029 } 1030 1031 func (r *ErrorResponse) Error() string { 1032 if r.Response != nil && r.Response.Request != nil { 1033 return fmt.Sprintf("%v %v: %d %v %+v", 1034 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1035 r.Response.StatusCode, r.Message, r.Errors) 1036 } 1037 1038 if r.Response != nil { 1039 return fmt.Sprintf("%d %v %+v", r.Response.StatusCode, r.Message, r.Errors) 1040 } 1041 1042 return fmt.Sprintf("%v %+v", r.Message, r.Errors) 1043 } 1044 1045 // Is returns whether the provided error equals this error. 1046 func (r *ErrorResponse) Is(target error) bool { 1047 v, ok := target.(*ErrorResponse) 1048 if !ok { 1049 return false 1050 } 1051 1052 if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) || 1053 !compareHTTPResponse(r.Response, v.Response) { 1054 return false 1055 } 1056 1057 // Compare Errors. 1058 if len(r.Errors) != len(v.Errors) { 1059 return false 1060 } 1061 for idx := range r.Errors { 1062 if r.Errors[idx] != v.Errors[idx] { 1063 return false 1064 } 1065 } 1066 1067 // Compare Block. 1068 if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) { 1069 return false 1070 } 1071 if r.Block != nil && v.Block != nil { 1072 if r.Block.Reason != v.Block.Reason { 1073 return false 1074 } 1075 if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt == 1076 nil && v.Block.CreatedAt != nil) { 1077 return false 1078 } 1079 if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil { 1080 if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) { 1081 return false 1082 } 1083 } 1084 } 1085 1086 return true 1087 } 1088 1089 // TwoFactorAuthError occurs when using HTTP Basic Authentication for a user 1090 // that has two-factor authentication enabled. The request can be reattempted 1091 // by providing a one-time password in the request. 1092 type TwoFactorAuthError ErrorResponse 1093 1094 func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() } 1095 1096 // RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit 1097 // remaining value of 0. 1098 type RateLimitError struct { 1099 Rate Rate // Rate specifies last known rate limit for the client 1100 Response *http.Response // HTTP response that caused this error 1101 Message string `json:"message"` // error message 1102 } 1103 1104 func (r *RateLimitError) Error() string { 1105 return fmt.Sprintf("%v %v: %d %v %v", 1106 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1107 r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time))) 1108 } 1109 1110 // Is returns whether the provided error equals this error. 1111 func (r *RateLimitError) Is(target error) bool { 1112 v, ok := target.(*RateLimitError) 1113 if !ok { 1114 return false 1115 } 1116 1117 return r.Rate == v.Rate && 1118 r.Message == v.Message && 1119 compareHTTPResponse(r.Response, v.Response) 1120 } 1121 1122 // AcceptedError occurs when GitHub returns 202 Accepted response with an 1123 // empty body, which means a job was scheduled on the GitHub side to process 1124 // the information needed and cache it. 1125 // Technically, 202 Accepted is not a real error, it's just used to 1126 // indicate that results are not ready yet, but should be available soon. 1127 // The request can be repeated after some time. 1128 type AcceptedError struct { 1129 // Raw contains the response body. 1130 Raw []byte 1131 } 1132 1133 func (*AcceptedError) Error() string { 1134 return "job scheduled on GitHub side; try again later" 1135 } 1136 1137 // Is returns whether the provided error equals this error. 1138 func (ae *AcceptedError) Is(target error) bool { 1139 v, ok := target.(*AcceptedError) 1140 if !ok { 1141 return false 1142 } 1143 return bytes.Equal(ae.Raw, v.Raw) 1144 } 1145 1146 // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the 1147 // "documentation_url" field value equal to "https://docs.github.com/rest/overview/rate-limits-for-the-rest-api#about-secondary-rate-limits". 1148 type AbuseRateLimitError struct { 1149 Response *http.Response // HTTP response that caused this error 1150 Message string `json:"message"` // error message 1151 1152 // RetryAfter is provided with some abuse rate limit errors. If present, 1153 // it is the amount of time that the client should wait before retrying. 1154 // Otherwise, the client should try again later (after an unspecified amount of time). 1155 RetryAfter *time.Duration 1156 } 1157 1158 func (r *AbuseRateLimitError) Error() string { 1159 return fmt.Sprintf("%v %v: %d %v", 1160 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 1161 r.Response.StatusCode, r.Message) 1162 } 1163 1164 // Is returns whether the provided error equals this error. 1165 func (r *AbuseRateLimitError) Is(target error) bool { 1166 v, ok := target.(*AbuseRateLimitError) 1167 if !ok { 1168 return false 1169 } 1170 1171 return r.Message == v.Message && 1172 r.RetryAfter == v.RetryAfter && 1173 compareHTTPResponse(r.Response, v.Response) 1174 } 1175 1176 // sanitizeURL redacts the client_secret parameter from the URL which may be 1177 // exposed to the user. 1178 func sanitizeURL(uri *url.URL) *url.URL { 1179 if uri == nil { 1180 return nil 1181 } 1182 params := uri.Query() 1183 if len(params.Get("client_secret")) > 0 { 1184 params.Set("client_secret", "REDACTED") 1185 uri.RawQuery = params.Encode() 1186 } 1187 return uri 1188 } 1189 1190 /* 1191 An Error reports more details on an individual error in an ErrorResponse. 1192 These are the possible validation error codes: 1193 1194 missing: 1195 resource does not exist 1196 missing_field: 1197 a required field on a resource has not been set 1198 invalid: 1199 the formatting of a field is invalid 1200 already_exists: 1201 another resource has the same valid as this field 1202 custom: 1203 some resources return this (e.g. github.User.CreateKey()), additional 1204 information is set in the Message field of the Error 1205 1206 GitHub error responses structure are often undocumented and inconsistent. 1207 Sometimes error is just a simple string (Issue #540). 1208 In such cases, Message represents an error message as a workaround. 1209 1210 GitHub API docs: https://docs.github.com/rest/#client-errors 1211 */ 1212 type Error struct { 1213 Resource string `json:"resource"` // resource on which the error occurred 1214 Field string `json:"field"` // field on which the error occurred 1215 Code string `json:"code"` // validation error code 1216 Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set. 1217 } 1218 1219 func (e *Error) Error() string { 1220 return fmt.Sprintf("%v error caused by %v field on %v resource", 1221 e.Code, e.Field, e.Resource) 1222 } 1223 1224 func (e *Error) UnmarshalJSON(data []byte) error { 1225 type aliasError Error // avoid infinite recursion by using type alias. 1226 if err := json.Unmarshal(data, (*aliasError)(e)); err != nil { 1227 return json.Unmarshal(data, &e.Message) // data can be json string. 1228 } 1229 return nil 1230 } 1231 1232 // CheckResponse checks the API response for errors, and returns them if 1233 // present. A response is considered an error if it has a status code outside 1234 // the 200 range or equal to 202 Accepted. 1235 // API error responses are expected to have response 1236 // body, and a JSON response body that maps to ErrorResponse. 1237 // 1238 // The error type will be *RateLimitError for rate limit exceeded errors, 1239 // *AcceptedError for 202 Accepted status codes, 1240 // and *TwoFactorAuthError for two-factor authentication errors. 1241 func CheckResponse(r *http.Response) error { 1242 if r.StatusCode == http.StatusAccepted { 1243 return &AcceptedError{} 1244 } 1245 if c := r.StatusCode; 200 <= c && c <= 299 { 1246 return nil 1247 } 1248 1249 errorResponse := &ErrorResponse{Response: r} 1250 data, err := io.ReadAll(r.Body) 1251 if err == nil && data != nil { 1252 err = json.Unmarshal(data, errorResponse) 1253 if err != nil { 1254 // reset the response as if this never happened 1255 errorResponse = &ErrorResponse{Response: r} 1256 } 1257 } 1258 // Re-populate error response body because GitHub error responses are often 1259 // undocumented and inconsistent. 1260 // Issue #1136, #540. 1261 r.Body = io.NopCloser(bytes.NewBuffer(data)) 1262 switch { 1263 case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"): 1264 return (*TwoFactorAuthError)(errorResponse) 1265 case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0": 1266 return &RateLimitError{ 1267 Rate: parseRate(r), 1268 Response: errorResponse.Response, 1269 Message: errorResponse.Message, 1270 } 1271 case r.StatusCode == http.StatusForbidden && 1272 (strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") || 1273 strings.HasSuffix(errorResponse.DocumentationURL, "secondary-rate-limits")): 1274 abuseRateLimitError := &AbuseRateLimitError{ 1275 Response: errorResponse.Response, 1276 Message: errorResponse.Message, 1277 } 1278 if retryAfter := parseSecondaryRate(r); retryAfter != nil { 1279 abuseRateLimitError.RetryAfter = retryAfter 1280 } 1281 return abuseRateLimitError 1282 default: 1283 return errorResponse 1284 } 1285 } 1286 1287 // parseBoolResponse determines the boolean result from a GitHub API response. 1288 // Several GitHub API methods return boolean responses indicated by the HTTP 1289 // status code in the response (true indicated by a 204, false indicated by a 1290 // 404). This helper function will determine that result and hide the 404 1291 // error if present. Any other error will be returned through as-is. 1292 func parseBoolResponse(err error) (bool, error) { 1293 if err == nil { 1294 return true, nil 1295 } 1296 1297 if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound { 1298 // Simply false. In this one case, we do not pass the error through. 1299 return false, nil 1300 } 1301 1302 // some other real error occurred 1303 return false, err 1304 } 1305 1306 type rateLimitCategory uint8 1307 1308 const ( 1309 coreCategory rateLimitCategory = iota 1310 searchCategory 1311 graphqlCategory 1312 integrationManifestCategory 1313 sourceImportCategory 1314 codeScanningUploadCategory 1315 actionsRunnerRegistrationCategory 1316 scimCategory 1317 dependencySnapshotsCategory 1318 codeSearchCategory 1319 1320 categories // An array of this length will be able to contain all rate limit categories. 1321 ) 1322 1323 // category returns the rate limit category of the endpoint, determined by HTTP method and Request.URL.Path. 1324 func category(method, path string) rateLimitCategory { 1325 switch { 1326 // https://docs.github.com/rest/rate-limit#about-rate-limits 1327 default: 1328 // NOTE: coreCategory is returned for actionsRunnerRegistrationCategory too, 1329 // because no API found for this category. 1330 return coreCategory 1331 1332 // https://docs.github.com/en/rest/search/search#search-code 1333 case strings.HasPrefix(path, "/search/code") && 1334 method == http.MethodGet: 1335 return codeSearchCategory 1336 1337 case strings.HasPrefix(path, "/search/"): 1338 return searchCategory 1339 case path == "/graphql": 1340 return graphqlCategory 1341 case strings.HasPrefix(path, "/app-manifests/") && 1342 strings.HasSuffix(path, "/conversions") && 1343 method == http.MethodPost: 1344 return integrationManifestCategory 1345 1346 // https://docs.github.com/rest/migrations/source-imports#start-an-import 1347 case strings.HasPrefix(path, "/repos/") && 1348 strings.HasSuffix(path, "/import") && 1349 method == http.MethodPut: 1350 return sourceImportCategory 1351 1352 // https://docs.github.com/rest/code-scanning#upload-an-analysis-as-sarif-data 1353 case strings.HasSuffix(path, "/code-scanning/sarifs"): 1354 return codeScanningUploadCategory 1355 1356 // https://docs.github.com/enterprise-cloud@latest/rest/scim 1357 case strings.HasPrefix(path, "/scim/"): 1358 return scimCategory 1359 1360 // https://docs.github.com/en/rest/dependency-graph/dependency-submission#create-a-snapshot-of-dependencies-for-a-repository 1361 case strings.HasPrefix(path, "/repos/") && 1362 strings.HasSuffix(path, "/dependency-graph/snapshots") && 1363 method == http.MethodPost: 1364 return dependencySnapshotsCategory 1365 } 1366 } 1367 1368 // RateLimits returns the rate limits for the current client. 1369 // 1370 // Deprecated: Use RateLimitService.Get instead. 1371 func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) { 1372 return c.RateLimit.Get(ctx) 1373 } 1374 1375 func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request { 1376 // To set extra headers, we must make a copy of the Request so 1377 // that we don't modify the Request we were given. This is required by the 1378 // specification of http.RoundTripper. 1379 // 1380 // Since we are going to modify only req.Header here, we only need a deep copy 1381 // of req.Header. 1382 convertedRequest := new(http.Request) 1383 *convertedRequest = *req 1384 convertedRequest.Header = make(http.Header, len(req.Header)) 1385 1386 for k, s := range req.Header { 1387 convertedRequest.Header[k] = append([]string(nil), s...) 1388 } 1389 convertedRequest.SetBasicAuth(id, secret) 1390 return convertedRequest 1391 } 1392 1393 /* 1394 UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls 1395 that need to use a higher rate limit associated with your OAuth application. 1396 1397 t := &github.UnauthenticatedRateLimitedTransport{ 1398 ClientID: "your app's client ID", 1399 ClientSecret: "your app's client secret", 1400 } 1401 client := github.NewClient(t.Client()) 1402 1403 This will add the client id and secret as a base64-encoded string in the format 1404 ClientID:ClientSecret and apply it as an "Authorization": "Basic" header. 1405 1406 See https://docs.github.com/rest/#unauthenticated-rate-limited-requests for 1407 more information. 1408 */ 1409 type UnauthenticatedRateLimitedTransport struct { 1410 // ClientID is the GitHub OAuth client ID of the current application, which 1411 // can be found by selecting its entry in the list at 1412 // https://github.com/settings/applications. 1413 ClientID string 1414 1415 // ClientSecret is the GitHub OAuth client secret of the current 1416 // application. 1417 ClientSecret string 1418 1419 // Transport is the underlying HTTP transport to use when making requests. 1420 // It will default to http.DefaultTransport if nil. 1421 Transport http.RoundTripper 1422 } 1423 1424 // RoundTrip implements the RoundTripper interface. 1425 func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 1426 if t.ClientID == "" { 1427 return nil, errors.New("t.ClientID is empty") 1428 } 1429 if t.ClientSecret == "" { 1430 return nil, errors.New("t.ClientSecret is empty") 1431 } 1432 1433 req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret) 1434 // Make the HTTP request. 1435 return t.transport().RoundTrip(req2) 1436 } 1437 1438 // Client returns an *http.Client that makes requests which are subject to the 1439 // rate limit of your OAuth application. 1440 func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client { 1441 return &http.Client{Transport: t} 1442 } 1443 1444 func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper { 1445 if t.Transport != nil { 1446 return t.Transport 1447 } 1448 return http.DefaultTransport 1449 } 1450 1451 // BasicAuthTransport is an http.RoundTripper that authenticates all requests 1452 // using HTTP Basic Authentication with the provided username and password. It 1453 // additionally supports users who have two-factor authentication enabled on 1454 // their GitHub account. 1455 type BasicAuthTransport struct { 1456 Username string // GitHub username 1457 Password string // GitHub password 1458 OTP string // one-time password for users with two-factor auth enabled 1459 1460 // Transport is the underlying HTTP transport to use when making requests. 1461 // It will default to http.DefaultTransport if nil. 1462 Transport http.RoundTripper 1463 } 1464 1465 // RoundTrip implements the RoundTripper interface. 1466 func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { 1467 req2 := setCredentialsAsHeaders(req, t.Username, t.Password) 1468 if t.OTP != "" { 1469 req2.Header.Set(headerOTP, t.OTP) 1470 } 1471 return t.transport().RoundTrip(req2) 1472 } 1473 1474 // Client returns an *http.Client that makes requests that are authenticated 1475 // using HTTP Basic Authentication. 1476 func (t *BasicAuthTransport) Client() *http.Client { 1477 return &http.Client{Transport: t} 1478 } 1479 1480 func (t *BasicAuthTransport) transport() http.RoundTripper { 1481 if t.Transport != nil { 1482 return t.Transport 1483 } 1484 return http.DefaultTransport 1485 } 1486 1487 // formatRateReset formats d to look like "[rate reset in 2s]" or 1488 // "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]" 1489 // for the negative cases. 1490 func formatRateReset(d time.Duration) string { 1491 isNegative := d < 0 1492 if isNegative { 1493 d *= -1 1494 } 1495 secondsTotal := int(0.5 + d.Seconds()) 1496 minutes := secondsTotal / 60 1497 seconds := secondsTotal - minutes*60 1498 1499 var timeString string 1500 if minutes > 0 { 1501 timeString = fmt.Sprintf("%dm%02ds", minutes, seconds) 1502 } else { 1503 timeString = fmt.Sprintf("%ds", seconds) 1504 } 1505 1506 if isNegative { 1507 return fmt.Sprintf("[rate limit was reset %v ago]", timeString) 1508 } 1509 return fmt.Sprintf("[rate reset in %v]", timeString) 1510 } 1511 1512 // When using roundTripWithOptionalFollowRedirect, note that it 1513 // is the responsibility of the caller to close the response body. 1514 func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, maxRedirects int, opts ...RequestOption) (*http.Response, error) { 1515 req, err := c.NewRequest("GET", u, nil, opts...) 1516 if err != nil { 1517 return nil, err 1518 } 1519 1520 var resp *http.Response 1521 // Use http.DefaultTransport if no custom Transport is configured 1522 req = withContext(ctx, req) 1523 if c.client.Transport == nil { 1524 resp, err = http.DefaultTransport.RoundTrip(req) 1525 } else { 1526 resp, err = c.client.Transport.RoundTrip(req) 1527 } 1528 if err != nil { 1529 return nil, err 1530 } 1531 1532 // If redirect response is returned, follow it 1533 if maxRedirects > 0 && resp.StatusCode == http.StatusMovedPermanently { 1534 _ = resp.Body.Close() 1535 u = resp.Header.Get("Location") 1536 resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, maxRedirects-1, opts...) 1537 } 1538 return resp, err 1539 } 1540 1541 // Bool is a helper routine that allocates a new bool value 1542 // to store v and returns a pointer to it. 1543 func Bool(v bool) *bool { return &v } 1544 1545 // Int is a helper routine that allocates a new int value 1546 // to store v and returns a pointer to it. 1547 func Int(v int) *int { return &v } 1548 1549 // Int64 is a helper routine that allocates a new int64 value 1550 // to store v and returns a pointer to it. 1551 func Int64(v int64) *int64 { return &v } 1552 1553 // String is a helper routine that allocates a new string value 1554 // to store v and returns a pointer to it. 1555 func String(v string) *string { return &v } 1556 1557 // roundTripperFunc creates a RoundTripper (transport) 1558 type roundTripperFunc func(*http.Request) (*http.Response, error) 1559 1560 func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { 1561 return fn(r) 1562 }