4
0

github.go 32 KB

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