123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989 |
- // 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/2015-03-09-licenses-api/
- mediaTypeLicensesPreview = "application/vnd.github.drax-preview+json"
- // https://developer.github.com/changes/2014-12-09-new-attributes-for-stars-api/
- mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
- // https://developer.github.com/changes/2015-11-11-protected-branches-api/
- mediaTypeProtectedBranchesPreview = "application/vnd.github.loki-preview+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/v3/apps/marketplace/
- mediaTypeMarketplacePreview = "application/vnd.github.valkyrie-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/2017-12-19-graphql-node-id/
- mediaTypeGraphQLNodeIDPreview = "application/vnd.github.jean-grey-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-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"
- )
- // 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
- 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.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
- 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 }
|