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 8package github 9 10import ( 11 "bytes" 12 "context" 13 "encoding/json" 14 "errors" 15 "fmt" 16 "io" 17 "io/ioutil" 18 "net/http" 19 "net/url" 20 "reflect" 21 "strconv" 22 "strings" 23 "sync" 24 "time" 25 26 "github.com/google/go-querystring/query" 27) 28 29const ( 30 defaultBaseURL = "https://api.github.com/" 31 uploadBaseURL = "https://uploads.github.com/" 32 userAgent = "go-github" 33 34 headerRateLimit = "X-RateLimit-Limit" 35 headerRateRemaining = "X-RateLimit-Remaining" 36 headerRateReset = "X-RateLimit-Reset" 37 headerOTP = "X-GitHub-OTP" 38 39 mediaTypeV3 = "application/vnd.github.v3+json" 40 defaultMediaType = "application/octet-stream" 41 mediaTypeV3SHA = "application/vnd.github.v3.sha" 42 mediaTypeV3Diff = "application/vnd.github.v3.diff" 43 mediaTypeV3Patch = "application/vnd.github.v3.patch" 44 mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json" 45 46 // Media Type values to access preview APIs 47 48 // https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/ 49 mediaTypeStarringPreview = "application/vnd.github.v3.star+json" 50 51 // https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/ 52 mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json" 53 54 // https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/ 55 mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json" 56 57 // https://developer.github.com/changes/2016-02-19-source-import-preview-api/ 58 mediaTypeImportPreview = "application/vnd.github.barred-rock-preview" 59 60 // https://developer.github.com/changes/2016-05-12-reactions-api-preview/ 61 mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview" 62 63 // https://developer.github.com/changes/2016-04-04-git-signing-api-preview/ 64 mediaTypeGitSigningPreview = "application/vnd.github.cryptographer-preview+json" 65 66 // https://developer.github.com/changes/2016-05-23-timeline-preview-api/ 67 mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json" 68 69 // https://developer.github.com/changes/2016-06-14-repository-invitations/ 70 mediaTypeRepositoryInvitationsPreview = "application/vnd.github.swamp-thing-preview+json" 71 72 // https://developer.github.com/changes/2016-07-06-github-pages-preiew-api/ 73 mediaTypePagesPreview = "application/vnd.github.mister-fantastic-preview+json" 74 75 // https://developer.github.com/changes/2016-09-14-projects-api/ 76 mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json" 77 78 // https://developer.github.com/changes/2016-09-14-Integrations-Early-Access/ 79 mediaTypeIntegrationPreview = "application/vnd.github.machine-man-preview+json" 80 81 // https://developer.github.com/changes/2017-01-05-commit-search-api/ 82 mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json" 83 84 // https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/ 85 mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json" 86 87 // https://developer.github.com/changes/2017-02-09-community-health/ 88 mediaTypeRepositoryCommunityHealthMetricsPreview = "application/vnd.github.black-panther-preview+json" 89 90 // https://developer.github.com/changes/2017-05-23-coc-api/ 91 mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json" 92 93 // https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/ 94 mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json" 95 96 // https://developer.github.com/changes/2017-08-30-preview-nested-teams/ 97 mediaTypeNestedTeamsPreview = "application/vnd.github.hellcat-preview+json" 98 99 // https://developer.github.com/changes/2017-11-09-repository-transfer-api-preview/ 100 mediaTypeRepositoryTransferPreview = "application/vnd.github.nightshade-preview+json" 101 102 // https://developer.github.com/changes/2018-01-25-organization-invitation-api-preview/ 103 mediaTypeOrganizationInvitationPreview = "application/vnd.github.dazzler-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-02-22-label-description-search-preview/ 109 mediaTypeLabelDescriptionSearchPreview = "application/vnd.github.symmetra-preview+json" 110 111 // https://developer.github.com/changes/2018-02-07-team-discussions-api/ 112 mediaTypeTeamDiscussionsPreview = "application/vnd.github.echo-preview+json" 113 114 // https://developer.github.com/changes/2018-03-21-hovercard-api-preview/ 115 mediaTypeHovercardPreview = "application/vnd.github.hagar-preview+json" 116 117 // https://developer.github.com/changes/2018-01-10-lock-reason-api-preview/ 118 mediaTypeLockReasonPreview = "application/vnd.github.sailor-v-preview+json" 119 120 // https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/ 121 mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json" 122 123 // https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/ 124 mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview" 125) 126 127// A Client manages communication with the GitHub API. 128type Client struct { 129 clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func. 130 client *http.Client // HTTP client used to communicate with the API. 131 132 // Base URL for API requests. Defaults to the public GitHub API, but can be 133 // set to a domain endpoint to use with GitHub Enterprise. BaseURL should 134 // always be specified with a trailing slash. 135 BaseURL *url.URL 136 137 // Base URL for uploading files. 138 UploadURL *url.URL 139 140 // User agent used when communicating with the GitHub API. 141 UserAgent string 142 143 rateMu sync.Mutex 144 rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls. 145 146 common service // Reuse a single struct instead of allocating one for each service on the heap. 147 148 // Services used for talking to different parts of the GitHub API. 149 Activity *ActivityService 150 Admin *AdminService 151 Apps *AppsService 152 Authorizations *AuthorizationsService 153 Checks *ChecksService 154 Gists *GistsService 155 Git *GitService 156 Gitignores *GitignoresService 157 Issues *IssuesService 158 Licenses *LicensesService 159 Marketplace *MarketplaceService 160 Migrations *MigrationService 161 Organizations *OrganizationsService 162 Projects *ProjectsService 163 PullRequests *PullRequestsService 164 Reactions *ReactionsService 165 Repositories *RepositoriesService 166 Search *SearchService 167 Teams *TeamsService 168 Users *UsersService 169} 170 171type service struct { 172 client *Client 173} 174 175// ListOptions specifies the optional parameters to various List methods that 176// support pagination. 177type ListOptions struct { 178 // For paginated result sets, page of results to retrieve. 179 Page int `url:"page,omitempty"` 180 181 // For paginated result sets, the number of results to include per page. 182 PerPage int `url:"per_page,omitempty"` 183} 184 185// UploadOptions specifies the parameters to methods that support uploads. 186type UploadOptions struct { 187 Name string `url:"name,omitempty"` 188} 189 190// RawType represents type of raw format of a request instead of JSON. 191type RawType uint8 192 193const ( 194 // Diff format. 195 Diff RawType = 1 + iota 196 // Patch format. 197 Patch 198) 199 200// RawOptions specifies parameters when user wants to get raw format of 201// a response instead of JSON. 202type RawOptions struct { 203 Type RawType 204} 205 206// addOptions adds the parameters in opt as URL query parameters to s. opt 207// must be a struct whose fields may contain "url" tags. 208func addOptions(s string, opt interface{}) (string, error) { 209 v := reflect.ValueOf(opt) 210 if v.Kind() == reflect.Ptr && v.IsNil() { 211 return s, nil 212 } 213 214 u, err := url.Parse(s) 215 if err != nil { 216 return s, err 217 } 218 219 qs, err := query.Values(opt) 220 if err != nil { 221 return s, err 222 } 223 224 u.RawQuery = qs.Encode() 225 return u.String(), nil 226} 227 228// NewClient returns a new GitHub API client. If a nil httpClient is 229// provided, http.DefaultClient will be used. To use API methods which require 230// authentication, provide an http.Client that will perform the authentication 231// for you (such as that provided by the golang.org/x/oauth2 library). 232func NewClient(httpClient *http.Client) *Client { 233 if httpClient == nil { 234 httpClient = http.DefaultClient 235 } 236 baseURL, _ := url.Parse(defaultBaseURL) 237 uploadURL, _ := url.Parse(uploadBaseURL) 238 239 c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL} 240 c.common.client = c 241 c.Activity = (*ActivityService)(&c.common) 242 c.Admin = (*AdminService)(&c.common) 243 c.Apps = (*AppsService)(&c.common) 244 c.Authorizations = (*AuthorizationsService)(&c.common) 245 c.Checks = (*ChecksService)(&c.common) 246 c.Gists = (*GistsService)(&c.common) 247 c.Git = (*GitService)(&c.common) 248 c.Gitignores = (*GitignoresService)(&c.common) 249 c.Issues = (*IssuesService)(&c.common) 250 c.Licenses = (*LicensesService)(&c.common) 251 c.Marketplace = &MarketplaceService{client: c} 252 c.Migrations = (*MigrationService)(&c.common) 253 c.Organizations = (*OrganizationsService)(&c.common) 254 c.Projects = (*ProjectsService)(&c.common) 255 c.PullRequests = (*PullRequestsService)(&c.common) 256 c.Reactions = (*ReactionsService)(&c.common) 257 c.Repositories = (*RepositoriesService)(&c.common) 258 c.Search = (*SearchService)(&c.common) 259 c.Teams = (*TeamsService)(&c.common) 260 c.Users = (*UsersService)(&c.common) 261 return c 262} 263 264// NewEnterpriseClient returns a new GitHub API client with provided 265// base URL and upload URL (often the same URL). 266// If either URL does not have a trailing slash, one is added automatically. 267// If a nil httpClient is provided, http.DefaultClient will be used. 268// 269// Note that NewEnterpriseClient is a convenience helper only; 270// its behavior is equivalent to using NewClient, followed by setting 271// the BaseURL and UploadURL fields. 272func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) { 273 baseEndpoint, err := url.Parse(baseURL) 274 if err != nil { 275 return nil, err 276 } 277 if !strings.HasSuffix(baseEndpoint.Path, "/") { 278 baseEndpoint.Path += "/" 279 } 280 281 uploadEndpoint, err := url.Parse(uploadURL) 282 if err != nil { 283 return nil, err 284 } 285 if !strings.HasSuffix(uploadEndpoint.Path, "/") { 286 uploadEndpoint.Path += "/" 287 } 288 289 c := NewClient(httpClient) 290 c.BaseURL = baseEndpoint 291 c.UploadURL = uploadEndpoint 292 return c, nil 293} 294 295// NewRequest creates an API request. A relative URL can be provided in urlStr, 296// in which case it is resolved relative to the BaseURL of the Client. 297// Relative URLs should always be specified without a preceding slash. If 298// specified, the value pointed to by body is JSON encoded and included as the 299// request body. 300func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { 301 if !strings.HasSuffix(c.BaseURL.Path, "/") { 302 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) 303 } 304 u, err := c.BaseURL.Parse(urlStr) 305 if err != nil { 306 return nil, err 307 } 308 309 var buf io.ReadWriter 310 if body != nil { 311 buf = new(bytes.Buffer) 312 enc := json.NewEncoder(buf) 313 enc.SetEscapeHTML(false) 314 err := enc.Encode(body) 315 if err != nil { 316 return nil, err 317 } 318 } 319 320 req, err := http.NewRequest(method, u.String(), buf) 321 if err != nil { 322 return nil, err 323 } 324 325 if body != nil { 326 req.Header.Set("Content-Type", "application/json") 327 } 328 req.Header.Set("Accept", mediaTypeV3) 329 if c.UserAgent != "" { 330 req.Header.Set("User-Agent", c.UserAgent) 331 } 332 return req, nil 333} 334 335// NewUploadRequest creates an upload request. A relative URL can be provided in 336// urlStr, in which case it is resolved relative to the UploadURL of the Client. 337// Relative URLs should always be specified without a preceding slash. 338func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) { 339 if !strings.HasSuffix(c.UploadURL.Path, "/") { 340 return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL) 341 } 342 u, err := c.UploadURL.Parse(urlStr) 343 if err != nil { 344 return nil, err 345 } 346 347 req, err := http.NewRequest("POST", u.String(), reader) 348 if err != nil { 349 return nil, err 350 } 351 req.ContentLength = size 352 353 if mediaType == "" { 354 mediaType = defaultMediaType 355 } 356 req.Header.Set("Content-Type", mediaType) 357 req.Header.Set("Accept", mediaTypeV3) 358 req.Header.Set("User-Agent", c.UserAgent) 359 return req, nil 360} 361 362// Response is a GitHub API response. This wraps the standard http.Response 363// returned from GitHub and provides convenient access to things like 364// pagination links. 365type Response struct { 366 *http.Response 367 368 // These fields provide the page values for paginating through a set of 369 // results. Any or all of these may be set to the zero value for 370 // responses that are not part of a paginated set, or for which there 371 // are no additional pages. 372 373 NextPage int 374 PrevPage int 375 FirstPage int 376 LastPage int 377 378 Rate 379} 380 381// newResponse creates a new Response for the provided http.Response. 382// r must not be nil. 383func newResponse(r *http.Response) *Response { 384 response := &Response{Response: r} 385 response.populatePageValues() 386 response.Rate = parseRate(r) 387 return response 388} 389 390// populatePageValues parses the HTTP Link response headers and populates the 391// various pagination link values in the Response. 392func (r *Response) populatePageValues() { 393 if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { 394 for _, link := range strings.Split(links[0], ",") { 395 segments := strings.Split(strings.TrimSpace(link), ";") 396 397 // link must at least have href and rel 398 if len(segments) < 2 { 399 continue 400 } 401 402 // ensure href is properly formatted 403 if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { 404 continue 405 } 406 407 // try to pull out page parameter 408 url, err := url.Parse(segments[0][1 : len(segments[0])-1]) 409 if err != nil { 410 continue 411 } 412 page := url.Query().Get("page") 413 if page == "" { 414 continue 415 } 416 417 for _, segment := range segments[1:] { 418 switch strings.TrimSpace(segment) { 419 case `rel="next"`: 420 r.NextPage, _ = strconv.Atoi(page) 421 case `rel="prev"`: 422 r.PrevPage, _ = strconv.Atoi(page) 423 case `rel="first"`: 424 r.FirstPage, _ = strconv.Atoi(page) 425 case `rel="last"`: 426 r.LastPage, _ = strconv.Atoi(page) 427 } 428 429 } 430 } 431 } 432} 433 434// parseRate parses the rate related headers. 435func parseRate(r *http.Response) Rate { 436 var rate Rate 437 if limit := r.Header.Get(headerRateLimit); limit != "" { 438 rate.Limit, _ = strconv.Atoi(limit) 439 } 440 if remaining := r.Header.Get(headerRateRemaining); remaining != "" { 441 rate.Remaining, _ = strconv.Atoi(remaining) 442 } 443 if reset := r.Header.Get(headerRateReset); reset != "" { 444 if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { 445 rate.Reset = Timestamp{time.Unix(v, 0)} 446 } 447 } 448 return rate 449} 450 451// Do sends an API request and returns the API response. The API response is 452// JSON decoded and stored in the value pointed to by v, or returned as an 453// error if an API error has occurred. If v implements the io.Writer 454// interface, the raw response body will be written to v, without attempting to 455// first decode it. If rate limit is exceeded and reset time is in the future, 456// Do returns *RateLimitError immediately without making a network API call. 457// 458// The provided ctx must be non-nil. If it is canceled or times out, 459// ctx.Err() will be returned. 460func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) { 461 req = withContext(ctx, req) 462 463 rateLimitCategory := category(req.URL.Path) 464 465 // If we've hit rate limit, don't make further requests before Reset time. 466 if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil { 467 return &Response{ 468 Response: err.Response, 469 Rate: err.Rate, 470 }, err 471 } 472 473 resp, err := c.client.Do(req) 474 if err != nil { 475 // If we got an error, and the context has been canceled, 476 // the context's error is probably more useful. 477 select { 478 case <-ctx.Done(): 479 return nil, ctx.Err() 480 default: 481 } 482 483 // If the error type is *url.Error, sanitize its URL before returning. 484 if e, ok := err.(*url.Error); ok { 485 if url, err := url.Parse(e.URL); err == nil { 486 e.URL = sanitizeURL(url).String() 487 return nil, e 488 } 489 } 490 491 return nil, err 492 } 493 defer resp.Body.Close() 494 495 response := newResponse(resp) 496 497 c.rateMu.Lock() 498 c.rateLimits[rateLimitCategory] = response.Rate 499 c.rateMu.Unlock() 500 501 err = CheckResponse(resp) 502 if err != nil { 503 // Even though there was an error, we still return the response 504 // in case the caller wants to inspect it further. 505 // However, if the error is AcceptedError, decode it below before 506 // returning from this function and closing the response body. 507 if _, ok := err.(*AcceptedError); !ok { 508 return response, err 509 } 510 } 511 512 if v != nil { 513 if w, ok := v.(io.Writer); ok { 514 io.Copy(w, resp.Body) 515 } else { 516 decErr := json.NewDecoder(resp.Body).Decode(v) 517 if decErr == io.EOF { 518 decErr = nil // ignore EOF errors caused by empty response body 519 } 520 if decErr != nil { 521 err = decErr 522 } 523 } 524 } 525 526 return response, err 527} 528 529// checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from 530// current client state in order to quickly check if *RateLimitError can be immediately returned 531// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily. 532// Otherwise it returns nil, and Client.Do should proceed normally. 533func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError { 534 c.rateMu.Lock() 535 rate := c.rateLimits[rateLimitCategory] 536 c.rateMu.Unlock() 537 if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) { 538 // Create a fake response. 539 resp := &http.Response{ 540 Status: http.StatusText(http.StatusForbidden), 541 StatusCode: http.StatusForbidden, 542 Request: req, 543 Header: make(http.Header), 544 Body: ioutil.NopCloser(strings.NewReader("")), 545 } 546 return &RateLimitError{ 547 Rate: rate, 548 Response: resp, 549 Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time), 550 } 551 } 552 553 return nil 554} 555 556/* 557An ErrorResponse reports one or more errors caused by an API request. 558 559GitHub API docs: https://developer.github.com/v3/#client-errors 560*/ 561type ErrorResponse struct { 562 Response *http.Response // HTTP response that caused this error 563 Message string `json:"message"` // error message 564 Errors []Error `json:"errors"` // more detail on individual errors 565 // Block is only populated on certain types of errors such as code 451. 566 // See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/ 567 // for more information. 568 Block *struct { 569 Reason string `json:"reason,omitempty"` 570 CreatedAt *Timestamp `json:"created_at,omitempty"` 571 } `json:"block,omitempty"` 572 // Most errors will also include a documentation_url field pointing 573 // to some content that might help you resolve the error, see 574 // https://developer.github.com/v3/#client-errors 575 DocumentationURL string `json:"documentation_url,omitempty"` 576} 577 578func (r *ErrorResponse) Error() string { 579 return fmt.Sprintf("%v %v: %d %v %+v", 580 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 581 r.Response.StatusCode, r.Message, r.Errors) 582} 583 584// TwoFactorAuthError occurs when using HTTP Basic Authentication for a user 585// that has two-factor authentication enabled. The request can be reattempted 586// by providing a one-time password in the request. 587type TwoFactorAuthError ErrorResponse 588 589func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() } 590 591// RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit 592// remaining value of 0, and error message starts with "API rate limit exceeded for ". 593type RateLimitError struct { 594 Rate Rate // Rate specifies last known rate limit for the client 595 Response *http.Response // HTTP response that caused this error 596 Message string `json:"message"` // error message 597} 598 599func (r *RateLimitError) Error() string { 600 return fmt.Sprintf("%v %v: %d %v %v", 601 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 602 r.Response.StatusCode, r.Message, formatRateReset(r.Rate.Reset.Time.Sub(time.Now()))) 603} 604 605// AcceptedError occurs when GitHub returns 202 Accepted response with an 606// empty body, which means a job was scheduled on the GitHub side to process 607// the information needed and cache it. 608// Technically, 202 Accepted is not a real error, it's just used to 609// indicate that results are not ready yet, but should be available soon. 610// The request can be repeated after some time. 611type AcceptedError struct{} 612 613func (*AcceptedError) Error() string { 614 return "job scheduled on GitHub side; try again later" 615} 616 617// AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the 618// "documentation_url" field value equal to "https://developer.github.com/v3/#abuse-rate-limits". 619type AbuseRateLimitError struct { 620 Response *http.Response // HTTP response that caused this error 621 Message string `json:"message"` // error message 622 623 // RetryAfter is provided with some abuse rate limit errors. If present, 624 // it is the amount of time that the client should wait before retrying. 625 // Otherwise, the client should try again later (after an unspecified amount of time). 626 RetryAfter *time.Duration 627} 628 629func (r *AbuseRateLimitError) Error() string { 630 return fmt.Sprintf("%v %v: %d %v", 631 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), 632 r.Response.StatusCode, r.Message) 633} 634 635// sanitizeURL redacts the client_secret parameter from the URL which may be 636// exposed to the user. 637func sanitizeURL(uri *url.URL) *url.URL { 638 if uri == nil { 639 return nil 640 } 641 params := uri.Query() 642 if len(params.Get("client_secret")) > 0 { 643 params.Set("client_secret", "REDACTED") 644 uri.RawQuery = params.Encode() 645 } 646 return uri 647} 648 649/* 650An Error reports more details on an individual error in an ErrorResponse. 651These are the possible validation error codes: 652 653 missing: 654 resource does not exist 655 missing_field: 656 a required field on a resource has not been set 657 invalid: 658 the formatting of a field is invalid 659 already_exists: 660 another resource has the same valid as this field 661 custom: 662 some resources return this (e.g. github.User.CreateKey()), additional 663 information is set in the Message field of the Error 664 665GitHub API docs: https://developer.github.com/v3/#client-errors 666*/ 667type Error struct { 668 Resource string `json:"resource"` // resource on which the error occurred 669 Field string `json:"field"` // field on which the error occurred 670 Code string `json:"code"` // validation error code 671 Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set. 672} 673 674func (e *Error) Error() string { 675 return fmt.Sprintf("%v error caused by %v field on %v resource", 676 e.Code, e.Field, e.Resource) 677} 678 679// CheckResponse checks the API response for errors, and returns them if 680// present. A response is considered an error if it has a status code outside 681// the 200 range or equal to 202 Accepted. 682// API error responses are expected to have either no response 683// body, or a JSON response body that maps to ErrorResponse. Any other 684// response body will be silently ignored. 685// 686// The error type will be *RateLimitError for rate limit exceeded errors, 687// *AcceptedError for 202 Accepted status codes, 688// and *TwoFactorAuthError for two-factor authentication errors. 689func CheckResponse(r *http.Response) error { 690 if r.StatusCode == http.StatusAccepted { 691 return &AcceptedError{} 692 } 693 if c := r.StatusCode; 200 <= c && c <= 299 { 694 return nil 695 } 696 errorResponse := &ErrorResponse{Response: r} 697 data, err := ioutil.ReadAll(r.Body) 698 if err == nil && data != nil { 699 json.Unmarshal(data, errorResponse) 700 } 701 switch { 702 case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"): 703 return (*TwoFactorAuthError)(errorResponse) 704 case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0" && strings.HasPrefix(errorResponse.Message, "API rate limit exceeded for "): 705 return &RateLimitError{ 706 Rate: parseRate(r), 707 Response: errorResponse.Response, 708 Message: errorResponse.Message, 709 } 710 case r.StatusCode == http.StatusForbidden && strings.HasSuffix(errorResponse.DocumentationURL, "/v3/#abuse-rate-limits"): 711 abuseRateLimitError := &AbuseRateLimitError{ 712 Response: errorResponse.Response, 713 Message: errorResponse.Message, 714 } 715 if v := r.Header["Retry-After"]; len(v) > 0 { 716 // According to GitHub support, the "Retry-After" header value will be 717 // an integer which represents the number of seconds that one should 718 // wait before resuming making requests. 719 retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop. 720 retryAfter := time.Duration(retryAfterSeconds) * time.Second 721 abuseRateLimitError.RetryAfter = &retryAfter 722 } 723 return abuseRateLimitError 724 default: 725 return errorResponse 726 } 727} 728 729// parseBoolResponse determines the boolean result from a GitHub API response. 730// Several GitHub API methods return boolean responses indicated by the HTTP 731// status code in the response (true indicated by a 204, false indicated by a 732// 404). This helper function will determine that result and hide the 404 733// error if present. Any other error will be returned through as-is. 734func parseBoolResponse(err error) (bool, error) { 735 if err == nil { 736 return true, nil 737 } 738 739 if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound { 740 // Simply false. In this one case, we do not pass the error through. 741 return false, nil 742 } 743 744 // some other real error occurred 745 return false, err 746} 747 748// Rate represents the rate limit for the current client. 749type Rate struct { 750 // The number of requests per hour the client is currently limited to. 751 Limit int `json:"limit"` 752 753 // The number of remaining requests the client can make this hour. 754 Remaining int `json:"remaining"` 755 756 // The time at which the current rate limit will reset. 757 Reset Timestamp `json:"reset"` 758} 759 760func (r Rate) String() string { 761 return Stringify(r) 762} 763 764// RateLimits represents the rate limits for the current client. 765type RateLimits struct { 766 // The rate limit for non-search API requests. Unauthenticated 767 // requests are limited to 60 per hour. Authenticated requests are 768 // limited to 5,000 per hour. 769 // 770 // GitHub API docs: https://developer.github.com/v3/#rate-limiting 771 Core *Rate `json:"core"` 772 773 // The rate limit for search API requests. Unauthenticated requests 774 // are limited to 10 requests per minutes. Authenticated requests are 775 // limited to 30 per minute. 776 // 777 // GitHub API docs: https://developer.github.com/v3/search/#rate-limit 778 Search *Rate `json:"search"` 779} 780 781func (r RateLimits) String() string { 782 return Stringify(r) 783} 784 785type rateLimitCategory uint8 786 787const ( 788 coreCategory rateLimitCategory = iota 789 searchCategory 790 791 categories // An array of this length will be able to contain all rate limit categories. 792) 793 794// category returns the rate limit category of the endpoint, determined by Request.URL.Path. 795func category(path string) rateLimitCategory { 796 switch { 797 default: 798 return coreCategory 799 case strings.HasPrefix(path, "/search/"): 800 return searchCategory 801 } 802} 803 804// RateLimits returns the rate limits for the current client. 805func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) { 806 req, err := c.NewRequest("GET", "rate_limit", nil) 807 if err != nil { 808 return nil, nil, err 809 } 810 811 response := new(struct { 812 Resources *RateLimits `json:"resources"` 813 }) 814 resp, err := c.Do(ctx, req, response) 815 if err != nil { 816 return nil, nil, err 817 } 818 819 if response.Resources != nil { 820 c.rateMu.Lock() 821 if response.Resources.Core != nil { 822 c.rateLimits[coreCategory] = *response.Resources.Core 823 } 824 if response.Resources.Search != nil { 825 c.rateLimits[searchCategory] = *response.Resources.Search 826 } 827 c.rateMu.Unlock() 828 } 829 830 return response.Resources, resp, nil 831} 832 833/* 834UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls 835that need to use a higher rate limit associated with your OAuth application. 836 837 t := &github.UnauthenticatedRateLimitedTransport{ 838 ClientID: "your app's client ID", 839 ClientSecret: "your app's client secret", 840 } 841 client := github.NewClient(t.Client()) 842 843This will append the querystring params client_id=xxx&client_secret=yyy to all 844requests. 845 846See https://developer.github.com/v3/#unauthenticated-rate-limited-requests for 847more information. 848*/ 849type UnauthenticatedRateLimitedTransport struct { 850 // ClientID is the GitHub OAuth client ID of the current application, which 851 // can be found by selecting its entry in the list at 852 // https://github.com/settings/applications. 853 ClientID string 854 855 // ClientSecret is the GitHub OAuth client secret of the current 856 // application. 857 ClientSecret string 858 859 // Transport is the underlying HTTP transport to use when making requests. 860 // It will default to http.DefaultTransport if nil. 861 Transport http.RoundTripper 862} 863 864// RoundTrip implements the RoundTripper interface. 865func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { 866 if t.ClientID == "" { 867 return nil, errors.New("t.ClientID is empty") 868 } 869 if t.ClientSecret == "" { 870 return nil, errors.New("t.ClientSecret is empty") 871 } 872 873 // To set extra querystring params, we must make a copy of the Request so 874 // that we don't modify the Request we were given. This is required by the 875 // specification of http.RoundTripper. 876 // 877 // Since we are going to modify only req.URL here, we only need a deep copy 878 // of req.URL. 879 req2 := new(http.Request) 880 *req2 = *req 881 req2.URL = new(url.URL) 882 *req2.URL = *req.URL 883 884 q := req2.URL.Query() 885 q.Set("client_id", t.ClientID) 886 q.Set("client_secret", t.ClientSecret) 887 req2.URL.RawQuery = q.Encode() 888 889 // Make the HTTP request. 890 return t.transport().RoundTrip(req2) 891} 892 893// Client returns an *http.Client that makes requests which are subject to the 894// rate limit of your OAuth application. 895func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client { 896 return &http.Client{Transport: t} 897} 898 899func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper { 900 if t.Transport != nil { 901 return t.Transport 902 } 903 return http.DefaultTransport 904} 905 906// BasicAuthTransport is an http.RoundTripper that authenticates all requests 907// using HTTP Basic Authentication with the provided username and password. It 908// additionally supports users who have two-factor authentication enabled on 909// their GitHub account. 910type BasicAuthTransport struct { 911 Username string // GitHub username 912 Password string // GitHub password 913 OTP string // one-time password for users with two-factor auth enabled 914 915 // Transport is the underlying HTTP transport to use when making requests. 916 // It will default to http.DefaultTransport if nil. 917 Transport http.RoundTripper 918} 919 920// RoundTrip implements the RoundTripper interface. 921func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) { 922 // To set extra headers, we must make a copy of the Request so 923 // that we don't modify the Request we were given. This is required by the 924 // specification of http.RoundTripper. 925 // 926 // Since we are going to modify only req.Header here, we only need a deep copy 927 // of req.Header. 928 req2 := new(http.Request) 929 *req2 = *req 930 req2.Header = make(http.Header, len(req.Header)) 931 for k, s := range req.Header { 932 req2.Header[k] = append([]string(nil), s...) 933 } 934 935 req2.SetBasicAuth(t.Username, t.Password) 936 if t.OTP != "" { 937 req2.Header.Set(headerOTP, t.OTP) 938 } 939 return t.transport().RoundTrip(req2) 940} 941 942// Client returns an *http.Client that makes requests that are authenticated 943// using HTTP Basic Authentication. 944func (t *BasicAuthTransport) Client() *http.Client { 945 return &http.Client{Transport: t} 946} 947 948func (t *BasicAuthTransport) transport() http.RoundTripper { 949 if t.Transport != nil { 950 return t.Transport 951 } 952 return http.DefaultTransport 953} 954 955// formatRateReset formats d to look like "[rate reset in 2s]" or 956// "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]" 957// for the negative cases. 958func formatRateReset(d time.Duration) string { 959 isNegative := d < 0 960 if isNegative { 961 d *= -1 962 } 963 secondsTotal := int(0.5 + d.Seconds()) 964 minutes := secondsTotal / 60 965 seconds := secondsTotal - minutes*60 966 967 var timeString string 968 if minutes > 0 { 969 timeString = fmt.Sprintf("%dm%02ds", minutes, seconds) 970 } else { 971 timeString = fmt.Sprintf("%ds", seconds) 972 } 973 974 if isNegative { 975 return fmt.Sprintf("[rate limit was reset %v ago]", timeString) 976 } 977 return fmt.Sprintf("[rate reset in %v]", timeString) 978} 979 980// Bool is a helper routine that allocates a new bool value 981// to store v and returns a pointer to it. 982func Bool(v bool) *bool { return &v } 983 984// Int is a helper routine that allocates a new int value 985// to store v and returns a pointer to it. 986func Int(v int) *int { return &v } 987 988// Int64 is a helper routine that allocates a new int64 value 989// to store v and returns a pointer to it. 990func Int64(v int64) *int64 { return &v } 991 992// String is a helper routine that allocates a new string value 993// to store v and returns a pointer to it. 994func String(v string) *string { return &v } 995