本站源代码
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1023 lines
34KB

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