mirror of
https://github.com/GoogleContainerTools/kaniko
synced 2026-10-01 17:17:43 +02:00
Copied over the release script from skaffold to get all new PRs that have been merged for the CHANGELOG. Ran dep ensure.
997 lines
33 KiB
Go
997 lines
33 KiB
Go
// Copyright 2013 The go-github AUTHORS. All rights reserved.
|
|
//
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
//go:generate go run gen-accessors.go
|
|
|
|
package github
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/go-querystring/query"
|
|
)
|
|
|
|
const (
|
|
defaultBaseURL = "https://api.github.com/"
|
|
uploadBaseURL = "https://uploads.github.com/"
|
|
userAgent = "go-github"
|
|
|
|
headerRateLimit = "X-RateLimit-Limit"
|
|
headerRateRemaining = "X-RateLimit-Remaining"
|
|
headerRateReset = "X-RateLimit-Reset"
|
|
headerOTP = "X-GitHub-OTP"
|
|
|
|
mediaTypeV3 = "application/vnd.github.v3+json"
|
|
defaultMediaType = "application/octet-stream"
|
|
mediaTypeV3SHA = "application/vnd.github.v3.sha"
|
|
mediaTypeV3Diff = "application/vnd.github.v3.diff"
|
|
mediaTypeV3Patch = "application/vnd.github.v3.patch"
|
|
mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
|
|
|
|
// Media Type values to access preview APIs
|
|
|
|
// https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/
|
|
mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
|
|
|
|
// https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
|
|
mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
|
|
mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-02-19-source-import-preview-api/
|
|
mediaTypeImportPreview = "application/vnd.github.barred-rock-preview"
|
|
|
|
// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
|
|
mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
|
|
|
|
// https://developer.github.com/changes/2016-04-04-git-signing-api-preview/
|
|
mediaTypeGitSigningPreview = "application/vnd.github.cryptographer-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-05-23-timeline-preview-api/
|
|
mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-06-14-repository-invitations/
|
|
mediaTypeRepositoryInvitationsPreview = "application/vnd.github.swamp-thing-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-07-06-github-pages-preiew-api/
|
|
mediaTypePagesPreview = "application/vnd.github.mister-fantastic-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-09-14-projects-api/
|
|
mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
|
|
|
|
// https://developer.github.com/changes/2016-09-14-Integrations-Early-Access/
|
|
mediaTypeIntegrationPreview = "application/vnd.github.machine-man-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-01-05-commit-search-api/
|
|
mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/
|
|
mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-02-09-community-health/
|
|
mediaTypeRepositoryCommunityHealthMetricsPreview = "application/vnd.github.black-panther-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-05-23-coc-api/
|
|
mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/
|
|
mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-08-30-preview-nested-teams/
|
|
mediaTypeNestedTeamsPreview = "application/vnd.github.hellcat-preview+json"
|
|
|
|
// https://developer.github.com/changes/2017-11-09-repository-transfer-api-preview/
|
|
mediaTypeRepositoryTransferPreview = "application/vnd.github.nightshade-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-01-25-organization-invitation-api-preview/
|
|
mediaTypeOrganizationInvitationPreview = "application/vnd.github.dazzler-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
|
|
mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-02-22-label-description-search-preview/
|
|
mediaTypeLabelDescriptionSearchPreview = "application/vnd.github.symmetra-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-02-07-team-discussions-api/
|
|
mediaTypeTeamDiscussionsPreview = "application/vnd.github.echo-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-03-21-hovercard-api-preview/
|
|
mediaTypeHovercardPreview = "application/vnd.github.hagar-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-01-10-lock-reason-api-preview/
|
|
mediaTypeLockReasonPreview = "application/vnd.github.sailor-v-preview+json"
|
|
|
|
// https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/
|
|
mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
|
|
|
|
// https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/
|
|
mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
|
|
)
|
|
|
|
// A Client manages communication with the GitHub API.
|
|
type Client struct {
|
|
clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func.
|
|
client *http.Client // HTTP client used to communicate with the API.
|
|
|
|
// Base URL for API requests. Defaults to the public GitHub API, but can be
|
|
// set to a domain endpoint to use with GitHub Enterprise. BaseURL should
|
|
// always be specified with a trailing slash.
|
|
BaseURL *url.URL
|
|
|
|
// Base URL for uploading files.
|
|
UploadURL *url.URL
|
|
|
|
// User agent used when communicating with the GitHub API.
|
|
UserAgent string
|
|
|
|
rateMu sync.Mutex
|
|
rateLimits [categories]Rate // Rate limits for the client as determined by the most recent API calls.
|
|
|
|
common service // Reuse a single struct instead of allocating one for each service on the heap.
|
|
|
|
// Services used for talking to different parts of the GitHub API.
|
|
Activity *ActivityService
|
|
Admin *AdminService
|
|
Apps *AppsService
|
|
Authorizations *AuthorizationsService
|
|
Checks *ChecksService
|
|
Gists *GistsService
|
|
Git *GitService
|
|
Gitignores *GitignoresService
|
|
Issues *IssuesService
|
|
Licenses *LicensesService
|
|
Marketplace *MarketplaceService
|
|
Migrations *MigrationService
|
|
Organizations *OrganizationsService
|
|
Projects *ProjectsService
|
|
PullRequests *PullRequestsService
|
|
Reactions *ReactionsService
|
|
Repositories *RepositoriesService
|
|
Search *SearchService
|
|
Teams *TeamsService
|
|
Users *UsersService
|
|
}
|
|
|
|
type service struct {
|
|
client *Client
|
|
}
|
|
|
|
// ListOptions specifies the optional parameters to various List methods that
|
|
// support pagination.
|
|
type ListOptions struct {
|
|
// For paginated result sets, page of results to retrieve.
|
|
Page int `url:"page,omitempty"`
|
|
|
|
// For paginated result sets, the number of results to include per page.
|
|
PerPage int `url:"per_page,omitempty"`
|
|
}
|
|
|
|
// UploadOptions specifies the parameters to methods that support uploads.
|
|
type UploadOptions struct {
|
|
Name string `url:"name,omitempty"`
|
|
}
|
|
|
|
// RawType represents type of raw format of a request instead of JSON.
|
|
type RawType uint8
|
|
|
|
const (
|
|
// Diff format.
|
|
Diff RawType = 1 + iota
|
|
// Patch format.
|
|
Patch
|
|
)
|
|
|
|
// RawOptions specifies parameters when user wants to get raw format of
|
|
// a response instead of JSON.
|
|
type RawOptions struct {
|
|
Type RawType
|
|
}
|
|
|
|
// addOptions adds the parameters in opt as URL query parameters to s. opt
|
|
// must be a struct whose fields may contain "url" tags.
|
|
func addOptions(s string, opt interface{}) (string, error) {
|
|
v := reflect.ValueOf(opt)
|
|
if v.Kind() == reflect.Ptr && v.IsNil() {
|
|
return s, nil
|
|
}
|
|
|
|
u, err := url.Parse(s)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
qs, err := query.Values(opt)
|
|
if err != nil {
|
|
return s, err
|
|
}
|
|
|
|
u.RawQuery = qs.Encode()
|
|
return u.String(), nil
|
|
}
|
|
|
|
// NewClient returns a new GitHub API client. If a nil httpClient is
|
|
// provided, http.DefaultClient will be used. To use API methods which require
|
|
// authentication, provide an http.Client that will perform the authentication
|
|
// for you (such as that provided by the golang.org/x/oauth2 library).
|
|
func NewClient(httpClient *http.Client) *Client {
|
|
if httpClient == nil {
|
|
httpClient = http.DefaultClient
|
|
}
|
|
baseURL, _ := url.Parse(defaultBaseURL)
|
|
uploadURL, _ := url.Parse(uploadBaseURL)
|
|
|
|
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL}
|
|
c.common.client = c
|
|
c.Activity = (*ActivityService)(&c.common)
|
|
c.Admin = (*AdminService)(&c.common)
|
|
c.Apps = (*AppsService)(&c.common)
|
|
c.Authorizations = (*AuthorizationsService)(&c.common)
|
|
c.Checks = (*ChecksService)(&c.common)
|
|
c.Gists = (*GistsService)(&c.common)
|
|
c.Git = (*GitService)(&c.common)
|
|
c.Gitignores = (*GitignoresService)(&c.common)
|
|
c.Issues = (*IssuesService)(&c.common)
|
|
c.Licenses = (*LicensesService)(&c.common)
|
|
c.Marketplace = &MarketplaceService{client: c}
|
|
c.Migrations = (*MigrationService)(&c.common)
|
|
c.Organizations = (*OrganizationsService)(&c.common)
|
|
c.Projects = (*ProjectsService)(&c.common)
|
|
c.PullRequests = (*PullRequestsService)(&c.common)
|
|
c.Reactions = (*ReactionsService)(&c.common)
|
|
c.Repositories = (*RepositoriesService)(&c.common)
|
|
c.Search = (*SearchService)(&c.common)
|
|
c.Teams = (*TeamsService)(&c.common)
|
|
c.Users = (*UsersService)(&c.common)
|
|
return c
|
|
}
|
|
|
|
// NewEnterpriseClient returns a new GitHub API client with provided
|
|
// base URL and upload URL (often the same URL).
|
|
// If either URL does not have a trailing slash, one is added automatically.
|
|
// If a nil httpClient is provided, http.DefaultClient will be used.
|
|
//
|
|
// Note that NewEnterpriseClient is a convenience helper only;
|
|
// its behavior is equivalent to using NewClient, followed by setting
|
|
// the BaseURL and UploadURL fields.
|
|
func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
|
|
baseEndpoint, err := url.Parse(baseURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !strings.HasSuffix(baseEndpoint.Path, "/") {
|
|
baseEndpoint.Path += "/"
|
|
}
|
|
|
|
uploadEndpoint, err := url.Parse(uploadURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !strings.HasSuffix(uploadEndpoint.Path, "/") {
|
|
uploadEndpoint.Path += "/"
|
|
}
|
|
|
|
c := NewClient(httpClient)
|
|
c.BaseURL = baseEndpoint
|
|
c.UploadURL = uploadEndpoint
|
|
return c, nil
|
|
}
|
|
|
|
// NewRequest creates an API request. A relative URL can be provided in urlStr,
|
|
// in which case it is resolved relative to the BaseURL of the Client.
|
|
// Relative URLs should always be specified without a preceding slash. If
|
|
// specified, the value pointed to by body is JSON encoded and included as the
|
|
// request body.
|
|
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
|
|
if !strings.HasSuffix(c.BaseURL.Path, "/") {
|
|
return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
|
|
}
|
|
u, err := c.BaseURL.Parse(urlStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf io.ReadWriter
|
|
if body != nil {
|
|
buf = new(bytes.Buffer)
|
|
enc := json.NewEncoder(buf)
|
|
enc.SetEscapeHTML(false)
|
|
err := enc.Encode(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest(method, u.String(), buf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req.Header.Set("Accept", mediaTypeV3)
|
|
if c.UserAgent != "" {
|
|
req.Header.Set("User-Agent", c.UserAgent)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// NewUploadRequest creates an upload request. A relative URL can be provided in
|
|
// urlStr, in which case it is resolved relative to the UploadURL of the Client.
|
|
// Relative URLs should always be specified without a preceding slash.
|
|
func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) {
|
|
if !strings.HasSuffix(c.UploadURL.Path, "/") {
|
|
return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
|
|
}
|
|
u, err := c.UploadURL.Parse(urlStr)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", u.String(), reader)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.ContentLength = size
|
|
|
|
if mediaType == "" {
|
|
mediaType = defaultMediaType
|
|
}
|
|
req.Header.Set("Content-Type", mediaType)
|
|
req.Header.Set("Accept", mediaTypeV3)
|
|
req.Header.Set("User-Agent", c.UserAgent)
|
|
return req, nil
|
|
}
|
|
|
|
// Response is a GitHub API response. This wraps the standard http.Response
|
|
// returned from GitHub and provides convenient access to things like
|
|
// pagination links.
|
|
type Response struct {
|
|
*http.Response
|
|
|
|
// These fields provide the page values for paginating through a set of
|
|
// results. Any or all of these may be set to the zero value for
|
|
// responses that are not part of a paginated set, or for which there
|
|
// are no additional pages.
|
|
|
|
NextPage int
|
|
PrevPage int
|
|
FirstPage int
|
|
LastPage int
|
|
|
|
// Explicitly specify the Rate type so Rate's String() receiver doesn't
|
|
// propagate to Response.
|
|
Rate Rate
|
|
}
|
|
|
|
// newResponse creates a new Response for the provided http.Response.
|
|
// r must not be nil.
|
|
func newResponse(r *http.Response) *Response {
|
|
response := &Response{Response: r}
|
|
response.populatePageValues()
|
|
response.Rate = parseRate(r)
|
|
return response
|
|
}
|
|
|
|
// populatePageValues parses the HTTP Link response headers and populates the
|
|
// various pagination link values in the Response.
|
|
func (r *Response) populatePageValues() {
|
|
if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
|
|
for _, link := range strings.Split(links[0], ",") {
|
|
segments := strings.Split(strings.TrimSpace(link), ";")
|
|
|
|
// link must at least have href and rel
|
|
if len(segments) < 2 {
|
|
continue
|
|
}
|
|
|
|
// ensure href is properly formatted
|
|
if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
|
|
continue
|
|
}
|
|
|
|
// try to pull out page parameter
|
|
url, err := url.Parse(segments[0][1 : len(segments[0])-1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
page := url.Query().Get("page")
|
|
if page == "" {
|
|
continue
|
|
}
|
|
|
|
for _, segment := range segments[1:] {
|
|
switch strings.TrimSpace(segment) {
|
|
case `rel="next"`:
|
|
r.NextPage, _ = strconv.Atoi(page)
|
|
case `rel="prev"`:
|
|
r.PrevPage, _ = strconv.Atoi(page)
|
|
case `rel="first"`:
|
|
r.FirstPage, _ = strconv.Atoi(page)
|
|
case `rel="last"`:
|
|
r.LastPage, _ = strconv.Atoi(page)
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// parseRate parses the rate related headers.
|
|
func parseRate(r *http.Response) Rate {
|
|
var rate Rate
|
|
if limit := r.Header.Get(headerRateLimit); limit != "" {
|
|
rate.Limit, _ = strconv.Atoi(limit)
|
|
}
|
|
if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
|
|
rate.Remaining, _ = strconv.Atoi(remaining)
|
|
}
|
|
if reset := r.Header.Get(headerRateReset); reset != "" {
|
|
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
|
|
rate.Reset = Timestamp{time.Unix(v, 0)}
|
|
}
|
|
}
|
|
return rate
|
|
}
|
|
|
|
// Do sends an API request and returns the API response. The API response is
|
|
// JSON decoded and stored in the value pointed to by v, or returned as an
|
|
// error if an API error has occurred. If v implements the io.Writer
|
|
// interface, the raw response body will be written to v, without attempting to
|
|
// first decode it. If rate limit is exceeded and reset time is in the future,
|
|
// Do returns *RateLimitError immediately without making a network API call.
|
|
//
|
|
// The provided ctx must be non-nil. If it is canceled or times out,
|
|
// ctx.Err() will be returned.
|
|
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
|
|
req = withContext(ctx, req)
|
|
|
|
rateLimitCategory := category(req.URL.Path)
|
|
|
|
// If we've hit rate limit, don't make further requests before Reset time.
|
|
if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
|
|
return &Response{
|
|
Response: err.Response,
|
|
Rate: err.Rate,
|
|
}, err
|
|
}
|
|
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
// If we got an error, and the context has been canceled,
|
|
// the context's error is probably more useful.
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// If the error type is *url.Error, sanitize its URL before returning.
|
|
if e, ok := err.(*url.Error); ok {
|
|
if url, err := url.Parse(e.URL); err == nil {
|
|
e.URL = sanitizeURL(url).String()
|
|
return nil, e
|
|
}
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
response := newResponse(resp)
|
|
|
|
c.rateMu.Lock()
|
|
c.rateLimits[rateLimitCategory] = response.Rate
|
|
c.rateMu.Unlock()
|
|
|
|
err = CheckResponse(resp)
|
|
if err != nil {
|
|
// Even though there was an error, we still return the response
|
|
// in case the caller wants to inspect it further.
|
|
// However, if the error is AcceptedError, decode it below before
|
|
// returning from this function and closing the response body.
|
|
if _, ok := err.(*AcceptedError); !ok {
|
|
return response, err
|
|
}
|
|
}
|
|
|
|
if v != nil {
|
|
if w, ok := v.(io.Writer); ok {
|
|
io.Copy(w, resp.Body)
|
|
} else {
|
|
decErr := json.NewDecoder(resp.Body).Decode(v)
|
|
if decErr == io.EOF {
|
|
decErr = nil // ignore EOF errors caused by empty response body
|
|
}
|
|
if decErr != nil {
|
|
err = decErr
|
|
}
|
|
}
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
// checkRateLimitBeforeDo does not make any network calls, but uses existing knowledge from
|
|
// current client state in order to quickly check if *RateLimitError can be immediately returned
|
|
// from Client.Do, and if so, returns it so that Client.Do can skip making a network API call unnecessarily.
|
|
// Otherwise it returns nil, and Client.Do should proceed normally.
|
|
func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError {
|
|
c.rateMu.Lock()
|
|
rate := c.rateLimits[rateLimitCategory]
|
|
c.rateMu.Unlock()
|
|
if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) {
|
|
// Create a fake response.
|
|
resp := &http.Response{
|
|
Status: http.StatusText(http.StatusForbidden),
|
|
StatusCode: http.StatusForbidden,
|
|
Request: req,
|
|
Header: make(http.Header),
|
|
Body: ioutil.NopCloser(strings.NewReader("")),
|
|
}
|
|
return &RateLimitError{
|
|
Rate: rate,
|
|
Response: resp,
|
|
Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time),
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
An ErrorResponse reports one or more errors caused by an API request.
|
|
|
|
GitHub API docs: https://developer.github.com/v3/#client-errors
|
|
*/
|
|
type ErrorResponse struct {
|
|
Response *http.Response // HTTP response that caused this error
|
|
Message string `json:"message"` // error message
|
|
Errors []Error `json:"errors"` // more detail on individual errors
|
|
// Block is only populated on certain types of errors such as code 451.
|
|
// See https://developer.github.com/changes/2016-03-17-the-451-status-code-is-now-supported/
|
|
// for more information.
|
|
Block *struct {
|
|
Reason string `json:"reason,omitempty"`
|
|
CreatedAt *Timestamp `json:"created_at,omitempty"`
|
|
} `json:"block,omitempty"`
|
|
// Most errors will also include a documentation_url field pointing
|
|
// to some content that might help you resolve the error, see
|
|
// https://developer.github.com/v3/#client-errors
|
|
DocumentationURL string `json:"documentation_url,omitempty"`
|
|
}
|
|
|
|
func (r *ErrorResponse) Error() string {
|
|
return fmt.Sprintf("%v %v: %d %v %+v",
|
|
r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
|
|
r.Response.StatusCode, r.Message, r.Errors)
|
|
}
|
|
|
|
// TwoFactorAuthError occurs when using HTTP Basic Authentication for a user
|
|
// that has two-factor authentication enabled. The request can be reattempted
|
|
// by providing a one-time password in the request.
|
|
type TwoFactorAuthError ErrorResponse
|
|
|
|
func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
|
|
|
|
// RateLimitError occurs when GitHub returns 403 Forbidden response with a rate limit
|
|
// remaining value of 0, and error message starts with "API rate limit exceeded for ".
|
|
type RateLimitError struct {
|
|
Rate Rate // Rate specifies last known rate limit for the client
|
|
Response *http.Response // HTTP response that caused this error
|
|
Message string `json:"message"` // error message
|
|
}
|
|
|
|
func (r *RateLimitError) Error() string {
|
|
return fmt.Sprintf("%v %v: %d %v %v",
|
|
r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
|
|
r.Response.StatusCode, r.Message, formatRateReset(r.Rate.Reset.Time.Sub(time.Now())))
|
|
}
|
|
|
|
// AcceptedError occurs when GitHub returns 202 Accepted response with an
|
|
// empty body, which means a job was scheduled on the GitHub side to process
|
|
// the information needed and cache it.
|
|
// Technically, 202 Accepted is not a real error, it's just used to
|
|
// indicate that results are not ready yet, but should be available soon.
|
|
// The request can be repeated after some time.
|
|
type AcceptedError struct{}
|
|
|
|
func (*AcceptedError) Error() string {
|
|
return "job scheduled on GitHub side; try again later"
|
|
}
|
|
|
|
// AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
|
|
// "documentation_url" field value equal to "https://developer.github.com/v3/#abuse-rate-limits".
|
|
type AbuseRateLimitError struct {
|
|
Response *http.Response // HTTP response that caused this error
|
|
Message string `json:"message"` // error message
|
|
|
|
// RetryAfter is provided with some abuse rate limit errors. If present,
|
|
// it is the amount of time that the client should wait before retrying.
|
|
// Otherwise, the client should try again later (after an unspecified amount of time).
|
|
RetryAfter *time.Duration
|
|
}
|
|
|
|
func (r *AbuseRateLimitError) Error() string {
|
|
return fmt.Sprintf("%v %v: %d %v",
|
|
r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
|
|
r.Response.StatusCode, r.Message)
|
|
}
|
|
|
|
// sanitizeURL redacts the client_secret parameter from the URL which may be
|
|
// exposed to the user.
|
|
func sanitizeURL(uri *url.URL) *url.URL {
|
|
if uri == nil {
|
|
return nil
|
|
}
|
|
params := uri.Query()
|
|
if len(params.Get("client_secret")) > 0 {
|
|
params.Set("client_secret", "REDACTED")
|
|
uri.RawQuery = params.Encode()
|
|
}
|
|
return uri
|
|
}
|
|
|
|
/*
|
|
An Error reports more details on an individual error in an ErrorResponse.
|
|
These are the possible validation error codes:
|
|
|
|
missing:
|
|
resource does not exist
|
|
missing_field:
|
|
a required field on a resource has not been set
|
|
invalid:
|
|
the formatting of a field is invalid
|
|
already_exists:
|
|
another resource has the same valid as this field
|
|
custom:
|
|
some resources return this (e.g. github.User.CreateKey()), additional
|
|
information is set in the Message field of the Error
|
|
|
|
GitHub API docs: https://developer.github.com/v3/#client-errors
|
|
*/
|
|
type Error struct {
|
|
Resource string `json:"resource"` // resource on which the error occurred
|
|
Field string `json:"field"` // field on which the error occurred
|
|
Code string `json:"code"` // validation error code
|
|
Message string `json:"message"` // Message describing the error. Errors with Code == "custom" will always have this set.
|
|
}
|
|
|
|
func (e *Error) Error() string {
|
|
return fmt.Sprintf("%v error caused by %v field on %v resource",
|
|
e.Code, e.Field, e.Resource)
|
|
}
|
|
|
|
// CheckResponse checks the API response for errors, and returns them if
|
|
// present. A response is considered an error if it has a status code outside
|
|
// the 200 range or equal to 202 Accepted.
|
|
// API error responses are expected to have either no response
|
|
// body, or a JSON response body that maps to ErrorResponse. Any other
|
|
// response body will be silently ignored.
|
|
//
|
|
// The error type will be *RateLimitError for rate limit exceeded errors,
|
|
// *AcceptedError for 202 Accepted status codes,
|
|
// and *TwoFactorAuthError for two-factor authentication errors.
|
|
func CheckResponse(r *http.Response) error {
|
|
if r.StatusCode == http.StatusAccepted {
|
|
return &AcceptedError{}
|
|
}
|
|
if c := r.StatusCode; 200 <= c && c <= 299 {
|
|
return nil
|
|
}
|
|
errorResponse := &ErrorResponse{Response: r}
|
|
data, err := ioutil.ReadAll(r.Body)
|
|
if err == nil && data != nil {
|
|
json.Unmarshal(data, errorResponse)
|
|
}
|
|
switch {
|
|
case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"):
|
|
return (*TwoFactorAuthError)(errorResponse)
|
|
case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0" && strings.HasPrefix(errorResponse.Message, "API rate limit exceeded for "):
|
|
return &RateLimitError{
|
|
Rate: parseRate(r),
|
|
Response: errorResponse.Response,
|
|
Message: errorResponse.Message,
|
|
}
|
|
case r.StatusCode == http.StatusForbidden && strings.HasSuffix(errorResponse.DocumentationURL, "/v3/#abuse-rate-limits"):
|
|
abuseRateLimitError := &AbuseRateLimitError{
|
|
Response: errorResponse.Response,
|
|
Message: errorResponse.Message,
|
|
}
|
|
if v := r.Header["Retry-After"]; len(v) > 0 {
|
|
// According to GitHub support, the "Retry-After" header value will be
|
|
// an integer which represents the number of seconds that one should
|
|
// wait before resuming making requests.
|
|
retryAfterSeconds, _ := strconv.ParseInt(v[0], 10, 64) // Error handling is noop.
|
|
retryAfter := time.Duration(retryAfterSeconds) * time.Second
|
|
abuseRateLimitError.RetryAfter = &retryAfter
|
|
}
|
|
return abuseRateLimitError
|
|
default:
|
|
return errorResponse
|
|
}
|
|
}
|
|
|
|
// parseBoolResponse determines the boolean result from a GitHub API response.
|
|
// Several GitHub API methods return boolean responses indicated by the HTTP
|
|
// status code in the response (true indicated by a 204, false indicated by a
|
|
// 404). This helper function will determine that result and hide the 404
|
|
// error if present. Any other error will be returned through as-is.
|
|
func parseBoolResponse(err error) (bool, error) {
|
|
if err == nil {
|
|
return true, nil
|
|
}
|
|
|
|
if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound {
|
|
// Simply false. In this one case, we do not pass the error through.
|
|
return false, nil
|
|
}
|
|
|
|
// some other real error occurred
|
|
return false, err
|
|
}
|
|
|
|
// Rate represents the rate limit for the current client.
|
|
type Rate struct {
|
|
// The number of requests per hour the client is currently limited to.
|
|
Limit int `json:"limit"`
|
|
|
|
// The number of remaining requests the client can make this hour.
|
|
Remaining int `json:"remaining"`
|
|
|
|
// The time at which the current rate limit will reset.
|
|
Reset Timestamp `json:"reset"`
|
|
}
|
|
|
|
func (r Rate) String() string {
|
|
return Stringify(r)
|
|
}
|
|
|
|
// RateLimits represents the rate limits for the current client.
|
|
type RateLimits struct {
|
|
// The rate limit for non-search API requests. Unauthenticated
|
|
// requests are limited to 60 per hour. Authenticated requests are
|
|
// limited to 5,000 per hour.
|
|
//
|
|
// GitHub API docs: https://developer.github.com/v3/#rate-limiting
|
|
Core *Rate `json:"core"`
|
|
|
|
// The rate limit for search API requests. Unauthenticated requests
|
|
// are limited to 10 requests per minutes. Authenticated requests are
|
|
// limited to 30 per minute.
|
|
//
|
|
// GitHub API docs: https://developer.github.com/v3/search/#rate-limit
|
|
Search *Rate `json:"search"`
|
|
}
|
|
|
|
func (r RateLimits) String() string {
|
|
return Stringify(r)
|
|
}
|
|
|
|
type rateLimitCategory uint8
|
|
|
|
const (
|
|
coreCategory rateLimitCategory = iota
|
|
searchCategory
|
|
|
|
categories // An array of this length will be able to contain all rate limit categories.
|
|
)
|
|
|
|
// category returns the rate limit category of the endpoint, determined by Request.URL.Path.
|
|
func category(path string) rateLimitCategory {
|
|
switch {
|
|
default:
|
|
return coreCategory
|
|
case strings.HasPrefix(path, "/search/"):
|
|
return searchCategory
|
|
}
|
|
}
|
|
|
|
// RateLimits returns the rate limits for the current client.
|
|
func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
|
|
req, err := c.NewRequest("GET", "rate_limit", nil)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
response := new(struct {
|
|
Resources *RateLimits `json:"resources"`
|
|
})
|
|
resp, err := c.Do(ctx, req, response)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if response.Resources != nil {
|
|
c.rateMu.Lock()
|
|
if response.Resources.Core != nil {
|
|
c.rateLimits[coreCategory] = *response.Resources.Core
|
|
}
|
|
if response.Resources.Search != nil {
|
|
c.rateLimits[searchCategory] = *response.Resources.Search
|
|
}
|
|
c.rateMu.Unlock()
|
|
}
|
|
|
|
return response.Resources, resp, nil
|
|
}
|
|
|
|
/*
|
|
UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls
|
|
that need to use a higher rate limit associated with your OAuth application.
|
|
|
|
t := &github.UnauthenticatedRateLimitedTransport{
|
|
ClientID: "your app's client ID",
|
|
ClientSecret: "your app's client secret",
|
|
}
|
|
client := github.NewClient(t.Client())
|
|
|
|
This will append the querystring params client_id=xxx&client_secret=yyy to all
|
|
requests.
|
|
|
|
See https://developer.github.com/v3/#unauthenticated-rate-limited-requests for
|
|
more information.
|
|
*/
|
|
type UnauthenticatedRateLimitedTransport struct {
|
|
// ClientID is the GitHub OAuth client ID of the current application, which
|
|
// can be found by selecting its entry in the list at
|
|
// https://github.com/settings/applications.
|
|
ClientID string
|
|
|
|
// ClientSecret is the GitHub OAuth client secret of the current
|
|
// application.
|
|
ClientSecret string
|
|
|
|
// Transport is the underlying HTTP transport to use when making requests.
|
|
// It will default to http.DefaultTransport if nil.
|
|
Transport http.RoundTripper
|
|
}
|
|
|
|
// RoundTrip implements the RoundTripper interface.
|
|
func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
if t.ClientID == "" {
|
|
return nil, errors.New("t.ClientID is empty")
|
|
}
|
|
if t.ClientSecret == "" {
|
|
return nil, errors.New("t.ClientSecret is empty")
|
|
}
|
|
|
|
// To set extra querystring params, we must make a copy of the Request so
|
|
// that we don't modify the Request we were given. This is required by the
|
|
// specification of http.RoundTripper.
|
|
//
|
|
// Since we are going to modify only req.URL here, we only need a deep copy
|
|
// of req.URL.
|
|
req2 := new(http.Request)
|
|
*req2 = *req
|
|
req2.URL = new(url.URL)
|
|
*req2.URL = *req.URL
|
|
|
|
q := req2.URL.Query()
|
|
q.Set("client_id", t.ClientID)
|
|
q.Set("client_secret", t.ClientSecret)
|
|
req2.URL.RawQuery = q.Encode()
|
|
|
|
// Make the HTTP request.
|
|
return t.transport().RoundTrip(req2)
|
|
}
|
|
|
|
// Client returns an *http.Client that makes requests which are subject to the
|
|
// rate limit of your OAuth application.
|
|
func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client {
|
|
return &http.Client{Transport: t}
|
|
}
|
|
|
|
func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
|
|
if t.Transport != nil {
|
|
return t.Transport
|
|
}
|
|
return http.DefaultTransport
|
|
}
|
|
|
|
// BasicAuthTransport is an http.RoundTripper that authenticates all requests
|
|
// using HTTP Basic Authentication with the provided username and password. It
|
|
// additionally supports users who have two-factor authentication enabled on
|
|
// their GitHub account.
|
|
type BasicAuthTransport struct {
|
|
Username string // GitHub username
|
|
Password string // GitHub password
|
|
OTP string // one-time password for users with two-factor auth enabled
|
|
|
|
// Transport is the underlying HTTP transport to use when making requests.
|
|
// It will default to http.DefaultTransport if nil.
|
|
Transport http.RoundTripper
|
|
}
|
|
|
|
// RoundTrip implements the RoundTripper interface.
|
|
func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
// To set extra headers, we must make a copy of the Request so
|
|
// that we don't modify the Request we were given. This is required by the
|
|
// specification of http.RoundTripper.
|
|
//
|
|
// Since we are going to modify only req.Header here, we only need a deep copy
|
|
// of req.Header.
|
|
req2 := new(http.Request)
|
|
*req2 = *req
|
|
req2.Header = make(http.Header, len(req.Header))
|
|
for k, s := range req.Header {
|
|
req2.Header[k] = append([]string(nil), s...)
|
|
}
|
|
|
|
req2.SetBasicAuth(t.Username, t.Password)
|
|
if t.OTP != "" {
|
|
req2.Header.Set(headerOTP, t.OTP)
|
|
}
|
|
return t.transport().RoundTrip(req2)
|
|
}
|
|
|
|
// Client returns an *http.Client that makes requests that are authenticated
|
|
// using HTTP Basic Authentication.
|
|
func (t *BasicAuthTransport) Client() *http.Client {
|
|
return &http.Client{Transport: t}
|
|
}
|
|
|
|
func (t *BasicAuthTransport) transport() http.RoundTripper {
|
|
if t.Transport != nil {
|
|
return t.Transport
|
|
}
|
|
return http.DefaultTransport
|
|
}
|
|
|
|
// formatRateReset formats d to look like "[rate reset in 2s]" or
|
|
// "[rate reset in 87m02s]" for the positive durations. And like "[rate limit was reset 87m02s ago]"
|
|
// for the negative cases.
|
|
func formatRateReset(d time.Duration) string {
|
|
isNegative := d < 0
|
|
if isNegative {
|
|
d *= -1
|
|
}
|
|
secondsTotal := int(0.5 + d.Seconds())
|
|
minutes := secondsTotal / 60
|
|
seconds := secondsTotal - minutes*60
|
|
|
|
var timeString string
|
|
if minutes > 0 {
|
|
timeString = fmt.Sprintf("%dm%02ds", minutes, seconds)
|
|
} else {
|
|
timeString = fmt.Sprintf("%ds", seconds)
|
|
}
|
|
|
|
if isNegative {
|
|
return fmt.Sprintf("[rate limit was reset %v ago]", timeString)
|
|
}
|
|
return fmt.Sprintf("[rate reset in %v]", timeString)
|
|
}
|
|
|
|
// Bool is a helper routine that allocates a new bool value
|
|
// to store v and returns a pointer to it.
|
|
func Bool(v bool) *bool { return &v }
|
|
|
|
// Int is a helper routine that allocates a new int value
|
|
// to store v and returns a pointer to it.
|
|
func Int(v int) *int { return &v }
|
|
|
|
// Int64 is a helper routine that allocates a new int64 value
|
|
// to store v and returns a pointer to it.
|
|
func Int64(v int64) *int64 { return &v }
|
|
|
|
// String is a helper routine that allocates a new string value
|
|
// to store v and returns a pointer to it.
|
|
func String(v string) *string { return &v }
|