github.go 33 KB

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