mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-28 10:04:49 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			1077 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
			
		
		
	
	
			1077 lines
		
	
	
		
			36 KiB
		
	
	
	
		
			Go
		
	
	
	
		
			Vendored
		
	
	
	
| // 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
 | |
| //go:generate go run gen-stringify-test.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/2018-10-16-deployments-environments-states-and-auto-inactive-updates/
 | |
| 	mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
 | |
| 	mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2016-05-23-timeline-preview-api/
 | |
| 	mediaTypeTimelinePreview = "application/vnd.github.mockingbird-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/2018-03-16-protected-branches-required-approving-reviews/
 | |
| 	mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-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"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/
 | |
| 	mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2018-09-05-project-card-events/
 | |
| 	mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2018-12-18-interactions-preview/
 | |
| 	mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/
 | |
| 	mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-04-24-vulnerability-alerts/
 | |
| 	mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-06-04-automated-security-fixes/
 | |
| 	mediaTypeRequiredAutomatedSecurityFixesPreview = "application/vnd.github.london-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-05-29-update-branch-api/
 | |
| 	mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/
 | |
| 	mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/v3/previews/#repository-creation-permissions
 | |
| 	mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/v3/previews/#create-and-use-repository-templates
 | |
| 	mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-10-03-multi-line-comments/
 | |
| 	mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/
 | |
| 	mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
 | |
| 
 | |
| 	// https://developer.github.com/changes/2019-12-03-internal-visibility-changes/
 | |
| 	mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-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.
 | |
| 	Actions        *ActionsService
 | |
| 	Activity       *ActivityService
 | |
| 	Admin          *AdminService
 | |
| 	Apps           *AppsService
 | |
| 	Authorizations *AuthorizationsService
 | |
| 	Checks         *ChecksService
 | |
| 	CodeScanning   *CodeScanningService
 | |
| 	Gists          *GistsService
 | |
| 	Git            *GitService
 | |
| 	Gitignores     *GitignoresService
 | |
| 	Interactions   *InteractionsService
 | |
| 	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 offset 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"`
 | |
| }
 | |
| 
 | |
| // ListCursorOptions specifies the optional parameters to various List methods that
 | |
| // support cursor pagination.
 | |
| type ListCursorOptions struct {
 | |
| 	// For paginated result sets, page of results to retrieve.
 | |
| 	Page string `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"`
 | |
| 	Label     string `url:"label,omitempty"`
 | |
| 	MediaType string `url:"-"`
 | |
| }
 | |
| 
 | |
| // 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 opts as URL query parameters to s. opts
 | |
| // must be a struct whose fields may contain "url" tags.
 | |
| func addOptions(s string, opts interface{}) (string, error) {
 | |
| 	v := reflect.ValueOf(opts)
 | |
| 	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(opts)
 | |
| 	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, a new http.Client 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.Client{}
 | |
| 	}
 | |
| 	baseURL, _ := url.Parse(defaultBaseURL)
 | |
| 	uploadURL, _ := url.Parse(uploadBaseURL)
 | |
| 
 | |
| 	c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent, UploadURL: uploadURL}
 | |
| 	c.common.client = c
 | |
| 	c.Actions = (*ActionsService)(&c.common)
 | |
| 	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.CodeScanning = (*CodeScanningService)(&c.common)
 | |
| 	c.Gists = (*GistsService)(&c.common)
 | |
| 	c.Git = (*GitService)(&c.common)
 | |
| 	c.Gitignores = (*GitignoresService)(&c.common)
 | |
| 	c.Interactions = (*InteractionsService)(&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 is your GitHub Enterprise hostname).
 | |
| // If the base URL does not have the suffix "/api/v3/", it will be added automatically.
 | |
| // If the upload URL does not have the suffix "/api/uploads", it will be added automatically.
 | |
| // If a nil httpClient is provided, a new http.Client 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.
 | |
| //
 | |
| // Another important thing is that by default, the GitHub Enterprise URL format
 | |
| // should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code.
 | |
| // The upload URL format should be http(s)://[hostname]/api/uploads/.
 | |
| 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 += "/"
 | |
| 	}
 | |
| 	if !strings.HasSuffix(baseEndpoint.Path, "/api/v3/") {
 | |
| 		baseEndpoint.Path += "api/v3/"
 | |
| 	}
 | |
| 
 | |
| 	uploadEndpoint, err := url.Parse(uploadURL)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	if !strings.HasSuffix(uploadEndpoint.Path, "/") {
 | |
| 		uploadEndpoint.Path += "/"
 | |
| 	}
 | |
| 	if !strings.HasSuffix(uploadEndpoint.Path, "/api/uploads/") {
 | |
| 		uploadEndpoint.Path += "api/uploads/"
 | |
| 	}
 | |
| 
 | |
| 	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 = &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.
 | |
| 	//
 | |
| 	// These fields support what is called "offset pagination" and should
 | |
| 	// be used with the ListOptions struct.
 | |
| 	NextPage  int
 | |
| 	PrevPage  int
 | |
| 	FirstPage int
 | |
| 	LastPage  int
 | |
| 
 | |
| 	// Additionally, some APIs support "cursor pagination" instead of offset.
 | |
| 	// This means that a token points directly to the next record which
 | |
| 	// can lead to O(1) performance compared to O(n) performance provided
 | |
| 	// by offset pagination.
 | |
| 	//
 | |
| 	// For APIs that support cursor pagination (such as
 | |
| 	// TeamsService.ListIDPGroupsInOrganization), the following field
 | |
| 	// will be populated to point to the next page.
 | |
| 	//
 | |
| 	// To use this token, set ListCursorOptions.Page to this value before
 | |
| 	// calling the endpoint again.
 | |
| 	NextPageToken string
 | |
| 
 | |
| 	// 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"`:
 | |
| 					if r.NextPage, err = strconv.Atoi(page); err != nil {
 | |
| 						r.NextPageToken = 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 nil an error is returned. 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) {
 | |
| 	if ctx == nil {
 | |
| 		return nil, errors.New("context must be non-nil")
 | |
| 	}
 | |
| 	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 {
 | |
| 		// Special case for AcceptedErrors. If an AcceptedError
 | |
| 		// has been encountered, the response's payload will be
 | |
| 		// added to the AcceptedError and returned.
 | |
| 		//
 | |
| 		// Issue #1022
 | |
| 		aerr, ok := err.(*AcceptedError)
 | |
| 		if ok {
 | |
| 			b, readErr := ioutil.ReadAll(resp.Body)
 | |
| 			if readErr != nil {
 | |
| 				return response, readErr
 | |
| 			}
 | |
| 
 | |
| 			aerr.Raw = b
 | |
| 			return response, aerr
 | |
| 		}
 | |
| 
 | |
| 		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.
 | |
| 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(time.Until(r.Rate.Reset.Time)))
 | |
| }
 | |
| 
 | |
| // 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 {
 | |
| 	// Raw contains the response body.
 | |
| 	Raw []byte
 | |
| }
 | |
| 
 | |
| 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 error responses structure are often undocumented and inconsistent.
 | |
| Sometimes error is just a simple string (Issue #540).
 | |
| In such cases, Message represents an error message as a workaround.
 | |
| 
 | |
| 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)
 | |
| }
 | |
| 
 | |
| func (e *Error) UnmarshalJSON(data []byte) error {
 | |
| 	type aliasError Error // avoid infinite recursion by using type alias.
 | |
| 	if err := json.Unmarshal(data, (*aliasError)(e)); err != nil {
 | |
| 		return json.Unmarshal(data, &e.Message) // data can be json string.
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // 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 response
 | |
| // body, and a JSON response body that maps to ErrorResponse.
 | |
| //
 | |
| // 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)
 | |
| 	}
 | |
| 	// Re-populate error response body because GitHub error responses are often
 | |
| 	// undocumented and inconsistent.
 | |
| 	// Issue #1136, #540.
 | |
| 	r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
 | |
| 	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":
 | |
| 		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
 | |
| }
 | |
| 
 | |
| func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
 | |
| 	// 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.
 | |
| 	convertedRequest := new(http.Request)
 | |
| 	*convertedRequest = *req
 | |
| 	convertedRequest.Header = make(http.Header, len(req.Header))
 | |
| 
 | |
| 	for k, s := range req.Header {
 | |
| 		convertedRequest.Header[k] = append([]string(nil), s...)
 | |
| 	}
 | |
| 	convertedRequest.SetBasicAuth(id, secret)
 | |
| 	return convertedRequest
 | |
| }
 | |
| 
 | |
| /*
 | |
| 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 add the client id and secret as a base64-encoded string in the format
 | |
| ClientID:ClientSecret and apply it as an "Authorization": "Basic" header.
 | |
| 
 | |
| 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")
 | |
| 	}
 | |
| 
 | |
| 	req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret)
 | |
| 	// 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) {
 | |
| 	req2 := setCredentialsAsHeaders(req, 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 }
 |