-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathgithub.go
More file actions
1868 lines (1619 loc) · 65.1 KB
/
github.go
File metadata and controls
1868 lines (1619 loc) · 65.1 KB
Edit and raw actions
OlderNewer
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
6
//go:generate go run gen-accessors.go
7
//go:generate go run gen-iterators.go
8
//go:generate go run gen-stringify-test.go
9
//go:generate sh ../script/metadata.sh update-go
10
11
package github
12
13
import (
14
"bytes"
15
"context"
16
"encoding/json"
17
"errors"
18
"fmt"
19
"io"
20
"net/http"
21
"net/url"
22
"regexp"
23
"strconv"
24
"strings"
25
"sync"
26
"time"
27
28
"github.com/google/go-querystring/query"
29
)
30
31
const (
32
Version = "v84.0.0"
33
34
HeaderRateLimit = "X-Ratelimit-Limit"
35
HeaderRateRemaining = "X-Ratelimit-Remaining"
36
HeaderRateReset = "X-Ratelimit-Reset"
37
HeaderRateResource = "X-Ratelimit-Resource"
38
HeaderRateUsed = "X-Ratelimit-Used"
39
HeaderRequestID = "X-Github-Request-Id"
40
41
defaultAPIVersion = "2022-11-28"
42
defaultBaseURL = "https://api.github.com/"
43
defaultUserAgent = "go-github" + "/" + Version
44
uploadBaseURL = "https://uploads.github.com/"
45
46
headerAPIVersion = "X-Github-Api-Version"
47
headerOTP = "X-Github-Otp"
48
headerRetryAfter = "Retry-After"
49
50
headerTokenExpiration = "Github-Authentication-Token-Expiration"
51
52
mediaTypeV3 = "application/vnd.github.v3+json"
53
defaultMediaType = "application/octet-stream"
54
mediaTypeV3SHA = "application/vnd.github.v3.sha"
55
mediaTypeV3Diff = "application/vnd.github.v3.diff"
56
mediaTypeV3Patch = "application/vnd.github.v3.patch"
57
mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
58
mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json"
59
mediaTypeStarring = "application/vnd.github.star+json"
60
mediaTypeSCIM = "application/scim+json"
61
62
// Media Type values to access preview APIs.
63
// These media types will be added to the API request as headers
64
// and used to enable particular features on GitHub API that are still in preview.
65
// After some time, specific media types will be promoted (to a "stable" state).
66
// From then on, the preview headers are not required anymore to activate the additional
67
// feature on GitHub.com's API. However, this API header might still be needed for users
68
// to run a GitHub Enterprise Server on-premise.
69
// It's not uncommon for GitHub Enterprise Server customers to run older versions which
70
// would probably rely on the preview headers for some time.
71
// While the header promotion is going out for GitHub.com, it may be some time before it
72
// even arrives in GitHub Enterprise Server.
73
// We keep those preview headers around to avoid breaking older GitHub Enterprise Server
74
// versions. Additionally, non-functional (preview) headers don't create any side effects
75
// on GitHub Cloud version.
76
//
77
// See https://github.com/google/go-github/pull/2125 and https://github.com/google/go-github/pull/2188 for full context.
78
79
// https://help.github.com/enterprise/2.4/admin/guides/migrations/exporting-the-github-com-organization-s-repositories/
80
mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
81
82
// https://developer.github.com/changes/2016-04-06-deployment-and-deployment-status-enhancements/
83
mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
84
85
// https://developer.github.com/changes/2018-10-16-deployments-environments-states-and-auto-inactive-updates/
86
mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
87
88
// https://developer.github.com/changes/2016-05-12-reactions-api-preview/
89
mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
90
91
// https://developer.github.com/changes/2016-05-23-timeline-preview-api/
92
mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
93
94
// https://developer.github.com/changes/2016-09-14-projects-api/
95
mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
96
97
// https://developer.github.com/changes/2017-01-05-commit-search-api/
98
mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
99
100
// https://developer.github.com/changes/2017-02-28-user-blocking-apis-and-webhook/
101
mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
102
103
// https://developer.github.com/changes/2017-05-23-coc-api/
104
mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
105
106
// https://developer.github.com/changes/2017-07-17-update-topics-on-repositories/
107
mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
108
109
// https://developer.github.com/changes/2018-03-16-protected-branches-required-approving-reviews/
110
mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
111
112
// https://developer.github.com/changes/2018-05-07-new-checks-api-public-beta/
113
mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
114
115
// https://developer.github.com/enterprise/2.13/v3/repos/pre_receive_hooks/
116
mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
117
118
// https://developer.github.com/changes/2018-02-22-protected-branches-required-signatures/
119
mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
120
121
// https://developer.github.com/changes/2018-09-05-project-card-events/
122
mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
123
124
// https://developer.github.com/changes/2018-12-18-interactions-preview/
125
mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
126
127
// https://developer.github.com/changes/2019-03-14-enabling-disabling-pages/
128
mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
129
130
// https://developer.github.com/changes/2019-04-24-vulnerability-alerts/
131
mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
132
133
// https://developer.github.com/changes/2019-05-29-update-branch-api/
134
mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
135
136
// https://developer.github.com/changes/2019-04-11-pulls-branches-for-commit/
137
mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
138
139
// https://docs.github.com/rest/previews/#repository-creation-permissions
140
mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
141
142
// https://docs.github.com/rest/previews/#create-and-use-repository-templates
143
mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
144
145
// https://developer.github.com/changes/2019-10-03-multi-line-comments/
146
mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
147
148
// https://developer.github.com/changes/2019-11-05-deprecated-passwords-and-authorizations-api/
149
mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
150
151
// https://developer.github.com/changes/2019-12-03-internal-visibility-changes/
152
mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json"
153
154
// https://developer.github.com/changes/2018-12-10-content-attachments-api/
155
mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json"
156
)
157
158
var errNonNilContext = errors.New("context must be non-nil")
159
160
// A Client manages communication with the GitHub API.
161
type Client struct {
162
clientMu sync.Mutex // clientMu protects the client during calls that modify the CheckRedirect func.
163
client *http.Client // HTTP client used to communicate with the API.
164
clientIgnoreRedirects *http.Client // HTTP client used to communicate with the API on endpoints where we don't want to follow redirects.
165
166
// Base URL for API requests. Defaults to the public GitHub API, but can be
167
// set to a domain endpoint to use with GitHub Enterprise. BaseURL should
168
// always be specified with a trailing slash.
169
BaseURL *url.URL
170
171
// Base URL for uploading files.
172
UploadURL *url.URL
173
174
// User agent used when communicating with the GitHub API.
175
UserAgent string
176
177
// DisableRateLimitCheck stops the client checking for rate limits or tracking
178
// them. This is different to setting BypassRateLimitCheck in the context,
179
// as that still tracks the rate limits.
180
DisableRateLimitCheck bool
181
182
rateMu sync.Mutex
183
rateLimits [Categories]Rate // Rate limits for the client as determined by the most recent API calls.
184
secondaryRateLimitReset time.Time // Secondary rate limit reset for the client as determined by the most recent API calls.
185
186
// If specified, Client will block requests for at most this duration in case of reaching a secondary
187
// rate limit
188
MaxSecondaryRateLimitRetryAfterDuration time.Duration
189
190
// Whether to respect rate limit headers on endpoints that return 302 redirections to artifacts
191
RateLimitRedirectionalEndpoints bool
192
193
common service // Reuse a single struct instead of allocating one for each service on the heap.
194
195
// Services used for talking to different parts of the GitHub API.
196
Actions *ActionsService
197
Activity *ActivityService
198
Admin *AdminService
199
Apps *AppsService
200
Authorizations *AuthorizationsService
201
Billing *BillingService
202
Checks *ChecksService
203
Classroom *ClassroomService
204
CodeScanning *CodeScanningService
205
CodesOfConduct *CodesOfConductService
206
Codespaces *CodespacesService
207
Copilot *CopilotService
208
Credentials *CredentialsService
209
Dependabot *DependabotService
210
DependencyGraph *DependencyGraphService
211
Emojis *EmojisService
212
Enterprise *EnterpriseService
213
Gists *GistsService
214
Git *GitService
215
Gitignores *GitignoresService
216
Interactions *InteractionsService
217
IssueImport *IssueImportService
218
Issues *IssuesService
219
Licenses *LicensesService
220
Markdown *MarkdownService
221
Marketplace *MarketplaceService
222
Meta *MetaService
223
Migrations *MigrationService
224
Organizations *OrganizationsService
225
PrivateRegistries *PrivateRegistriesService
226
Projects *ProjectsService
227
PullRequests *PullRequestsService
228
RateLimit *RateLimitService
229
Reactions *ReactionsService
230
Repositories *RepositoriesService
231
SCIM *SCIMService
232
Search *SearchService
233
SecretScanning *SecretScanningService
234
SecurityAdvisories *SecurityAdvisoriesService
235
SubIssue *SubIssueService
236
Teams *TeamsService
237
Users *UsersService
238
}
239
240
type service struct {
241
client *Client
242
}
243
244
// Client returns the http.Client used by this GitHub client.
245
// This should only be used for requests to the GitHub API because
246
// request headers will contain an authorization token.
247
func (c *Client) Client() *http.Client {
248
c.clientMu.Lock()
249
defer c.clientMu.Unlock()
250
clientCopy := *c.client
251
return &clientCopy
252
}
253
254
// ListOptions specifies the optional parameters to various List methods that
255
// support offset pagination.
256
type ListOptions struct {
257
// For paginated result sets, page of results to retrieve.
258
Page int `url:"page,omitempty"`
259
260
// For paginated result sets, the number of results to include per page.
261
PerPage int `url:"per_page,omitempty"`
262
}
263
264
// ListCursorOptions specifies the optional parameters to various List methods that
265
// support cursor pagination.
266
type ListCursorOptions struct {
267
// For paginated result sets, page of results to retrieve.
268
Page string `url:"page,omitempty"`
269
270
// For paginated result sets, the number of results to include per page.
271
PerPage int `url:"per_page,omitempty"`
272
273
// For paginated result sets, the number of results per page (max 100), starting from the first matching result.
274
// This parameter must not be used in combination with last.
275
First int `url:"first,omitempty"`
276
277
// For paginated result sets, the number of results per page (max 100), starting from the last matching result.
278
// This parameter must not be used in combination with first.
279
Last int `url:"last,omitempty"`
280
281
// A cursor, as given in the Link header. If specified, the query only searches for events after this cursor.
282
After string `url:"after,omitempty"`
283
284
// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
285
Before string `url:"before,omitempty"`
286
287
// A cursor, as given in the Link header. If specified, the query continues the search using this cursor.
288
Cursor string `url:"cursor,omitempty"`
289
}
290
291
// UploadOptions specifies the parameters to methods that support uploads.
292
type UploadOptions struct {
293
Name string `url:"name,omitempty"`
294
Label string `url:"label,omitempty"`
295
MediaType string `url:"-"`
296
}
297
298
// RawType represents type of raw format of a request instead of JSON.
299
type RawType uint8
300
301
const (
302
// Diff format.
303
Diff RawType = 1 + iota
304
// Patch format.
305
Patch
306
)
307
308
// RawOptions specifies parameters when user wants to get raw format of
309
// a response instead of JSON.
310
type RawOptions struct {
311
Type RawType
312
}
313
314
type structPtr[T any] interface{ *T }
315
316
// addOptions adds the parameters in opts as URL query parameters to s. opts
317
// must be a struct whose fields may contain "url" tags.
318
func addOptions[P structPtr[T], T any](s string, opts P) (string, error) {
319
if opts == nil {
320
return s, nil
321
}
322
323
u, err := url.Parse(s)
324
if err != nil {
325
return s, err
326
}
327
328
qs, err := query.Values(opts)
329
if err != nil {
330
return s, err
331
}
332
333
u.RawQuery = qs.Encode()
334
return u.String(), nil
335
}
336
337
// NewClient returns a new GitHub API client. If a nil httpClient is
338
// provided, a new http.Client will be used. To use API methods which require
339
// authentication, either use Client.WithAuthToken or provide NewClient with
340
// an http.Client that will perform the authentication for you (such as that
341
// provided by the golang.org/x/oauth2 library).
342
//
343
// Note: When using a nil httpClient, the default client has no timeout set.
344
// This may not be suitable for production environments. It is recommended to
345
// provide a custom http.Client with an appropriate timeout.
346
func NewClient(httpClient *http.Client) *Client {
347
if httpClient == nil {
348
httpClient = &http.Client{}
349
}
350
httpClient2 := *httpClient
351
c := &Client{client: &httpClient2}
352
c.initialize()
353
return c
354
}
355
356
// WithAuthToken returns a copy of the client configured to use the provided token for the Authorization header.
357
func (c *Client) WithAuthToken(token string) *Client {
358
c2 := c.copy()
359
defer c2.initialize()
360
transport := c2.client.Transport
361
if transport == nil {
362
transport = http.DefaultTransport
363
}
364
c2.client.Transport = roundTripperFunc(
365
func(req *http.Request) (*http.Response, error) {
366
req = req.Clone(req.Context())
367
if token != "" {
368
req.Header.Set("Authorization", fmt.Sprintf("Bearer %v", token))
369
}
370
return transport.RoundTrip(req)
371
},
372
)
373
return c2
374
}
375
376
// WithEnterpriseURLs returns a copy of the client configured to use the provided base and
377
// upload URLs. If the base URL does not have the suffix "/api/v3/", it will be added
378
// automatically. If the upload URL does not have the suffix "/api/uploads", it will be
379
// added automatically.
380
//
381
// Note that WithEnterpriseURLs is a convenience helper only;
382
// its behavior is equivalent to setting the BaseURL and UploadURL fields.
383
//
384
// Another important thing is that by default, the GitHub Enterprise URL format
385
// should be http(s)://[hostname]/api/v3/ or you will always receive the 406 status code.
386
// The upload URL format should be http(s)://[hostname]/api/uploads/.
387
func (c *Client) WithEnterpriseURLs(baseURL, uploadURL string) (*Client, error) {
388
c2 := c.copy()
389
defer c2.initialize()
390
var err error
391
c2.BaseURL, err = url.Parse(baseURL)
392
if err != nil {
393
return nil, err
394
}
395
396
if !strings.HasSuffix(c2.BaseURL.Path, "/") {
397
c2.BaseURL.Path += "/"
398
}
399
if !strings.HasSuffix(c2.BaseURL.Path, "/api/v3/") &&
400
!strings.HasPrefix(c2.BaseURL.Host, "api.") &&
401
!strings.Contains(c2.BaseURL.Host, ".api.") {
402
c2.BaseURL.Path += "api/v3/"
403
}
404
405
c2.UploadURL, err = url.Parse(uploadURL)
406
if err != nil {
407
return nil, err
408
}
409
410
if !strings.HasSuffix(c2.UploadURL.Path, "/") {
411
c2.UploadURL.Path += "/"
412
}
413
if !strings.HasSuffix(c2.UploadURL.Path, "/api/uploads/") &&
414
!strings.HasPrefix(c2.UploadURL.Host, "api.") &&
415
!strings.Contains(c2.UploadURL.Host, ".api.") &&
416
!strings.HasPrefix(c2.UploadURL.Host, "uploads.") {
417
c2.UploadURL.Path += "api/uploads/"
418
}
419
return c2, nil
420
}
421
422
// initialize sets default values and initializes services.
423
func (c *Client) initialize() {
424
if c.client == nil {
425
c.client = &http.Client{}
426
}
427
// Copy the main http client into the IgnoreRedirects one, overriding the `CheckRedirect` func
428
c.clientIgnoreRedirects = &http.Client{}
429
c.clientIgnoreRedirects.Transport = c.client.Transport
430
c.clientIgnoreRedirects.Timeout = c.client.Timeout
431
c.clientIgnoreRedirects.Jar = c.client.Jar
432
c.clientIgnoreRedirects.CheckRedirect = func(*http.Request, []*http.Request) error {
433
return http.ErrUseLastResponse
434
}
435
if c.BaseURL == nil {
436
c.BaseURL, _ = url.Parse(defaultBaseURL)
437
}
438
if c.UploadURL == nil {
439
c.UploadURL, _ = url.Parse(uploadBaseURL)
440
}
441
if c.UserAgent == "" {
442
c.UserAgent = defaultUserAgent
443
}
444
c.common.client = c
445
c.Actions = (*ActionsService)(&c.common)
446
c.Activity = (*ActivityService)(&c.common)
447
c.Admin = (*AdminService)(&c.common)
448
c.Apps = (*AppsService)(&c.common)
449
c.Authorizations = (*AuthorizationsService)(&c.common)
450
c.Billing = (*BillingService)(&c.common)
451
c.Checks = (*ChecksService)(&c.common)
452
c.Classroom = (*ClassroomService)(&c.common)
453
c.CodeScanning = (*CodeScanningService)(&c.common)
454
c.Codespaces = (*CodespacesService)(&c.common)
455
c.CodesOfConduct = (*CodesOfConductService)(&c.common)
456
c.Copilot = (*CopilotService)(&c.common)
457
c.Credentials = (*CredentialsService)(&c.common)
458
c.Dependabot = (*DependabotService)(&c.common)
459
c.DependencyGraph = (*DependencyGraphService)(&c.common)
460
c.Emojis = (*EmojisService)(&c.common)
461
c.Enterprise = (*EnterpriseService)(&c.common)
462
c.Gists = (*GistsService)(&c.common)
463
c.Git = (*GitService)(&c.common)
464
c.Gitignores = (*GitignoresService)(&c.common)
465
c.Interactions = (*InteractionsService)(&c.common)
466
c.IssueImport = (*IssueImportService)(&c.common)
467
c.Issues = (*IssuesService)(&c.common)
468
c.Licenses = (*LicensesService)(&c.common)
469
c.Markdown = (*MarkdownService)(&c.common)
470
c.Marketplace = &MarketplaceService{client: c}
471
c.Meta = (*MetaService)(&c.common)
472
c.Migrations = (*MigrationService)(&c.common)
473
c.Organizations = (*OrganizationsService)(&c.common)
474
c.PrivateRegistries = (*PrivateRegistriesService)(&c.common)
475
c.Projects = (*ProjectsService)(&c.common)
476
c.PullRequests = (*PullRequestsService)(&c.common)
477
c.RateLimit = (*RateLimitService)(&c.common)
478
c.Reactions = (*ReactionsService)(&c.common)
479
c.Repositories = (*RepositoriesService)(&c.common)
480
c.SCIM = (*SCIMService)(&c.common)
481
c.Search = (*SearchService)(&c.common)
482
c.SecretScanning = (*SecretScanningService)(&c.common)
483
c.SecurityAdvisories = (*SecurityAdvisoriesService)(&c.common)
484
c.SubIssue = (*SubIssueService)(&c.common)
485
c.Teams = (*TeamsService)(&c.common)
486
c.Users = (*UsersService)(&c.common)
487
}
488
489
// copy returns a copy of the current client. It must be initialized before use.
490
func (c *Client) copy() *Client {
491
c.clientMu.Lock()
492
// can't use *c here because that would copy mutexes by value.
493
clone := Client{
494
client: &http.Client{},
495
UserAgent: c.UserAgent,
496
BaseURL: c.BaseURL,
497
UploadURL: c.UploadURL,
498
RateLimitRedirectionalEndpoints: c.RateLimitRedirectionalEndpoints,
499
secondaryRateLimitReset: c.secondaryRateLimitReset,
500
}
501
c.clientMu.Unlock()
502
if c.client != nil {
503
clone.client.Transport = c.client.Transport
504
clone.client.CheckRedirect = c.client.CheckRedirect
505
clone.client.Jar = c.client.Jar
506
clone.client.Timeout = c.client.Timeout
507
}
508
c.rateMu.Lock()
509
copy(clone.rateLimits[:], c.rateLimits[:])
510
c.rateMu.Unlock()
511
return &clone
512
}
513
514
// NewClientWithEnvProxy enhances NewClient with the HttpProxy env.
515
func NewClientWithEnvProxy() *Client {
516
return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}})
517
}
518
519
// NewTokenClient returns a new GitHub API client authenticated with the provided token.
520
//
521
// Deprecated: Use NewClient(nil).WithAuthToken(token) instead.
522
func NewTokenClient(_ context.Context, token string) *Client {
523
// This always returns a nil error.
524
return NewClient(nil).WithAuthToken(token)
525
}
526
527
// NewEnterpriseClient returns a new GitHub API client with provided
528
// base URL and upload URL (often is your GitHub Enterprise hostname).
529
//
530
// Deprecated: Use NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL) instead.
531
func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
532
return NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL)
533
}
534
535
// RequestOption represents an option that can modify an http.Request.
536
type RequestOption func(req *http.Request)
537
538
// WithVersion overrides the GitHub v3 API version for this individual request.
539
// For more information, see:
540
// https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/
541
func WithVersion(version string) RequestOption {
542
return func(req *http.Request) {
543
req.Header.Set(headerAPIVersion, version)
544
}
545
}
546
547
// NewRequest creates an API request. A relative URL can be provided in urlStr,
548
// in which case it is resolved relative to the BaseURL of the Client.
549
// Relative URLs should always be specified without a preceding slash. If
550
// specified, the value pointed to by body is JSON encoded and included as the
551
// request body.
552
func (c *Client) NewRequest(method, urlStr string, body any, opts ...RequestOption) (*http.Request, error) {
553
if !strings.HasSuffix(c.BaseURL.Path, "/") {
554
return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.BaseURL)
555
}
556
557
u, err := c.BaseURL.Parse(urlStr)
558
if err != nil {
559
return nil, err
560
}
561
562
var buf io.ReadWriter
563
if body != nil {
564
buf = &bytes.Buffer{}
565
enc := json.NewEncoder(buf)
566
enc.SetEscapeHTML(false)
567
err := enc.Encode(body)
568
if err != nil {
569
return nil, err
570
}
571
}
572
573
req, err := http.NewRequest(method, u.String(), buf)
574
if err != nil {
575
return nil, err
576
}
577
578
if body != nil {
579
req.Header.Set("Content-Type", "application/json")
580
}
581
req.Header.Set("Accept", mediaTypeV3)
582
if c.UserAgent != "" {
583
req.Header.Set("User-Agent", c.UserAgent)
584
}
585
req.Header.Set(headerAPIVersion, defaultAPIVersion)
586
587
for _, opt := range opts {
588
opt(req)
589
}
590
591
return req, nil
592
}
593
594
// NewFormRequest creates an API request. A relative URL can be provided in urlStr,
595
// in which case it is resolved relative to the BaseURL of the Client.
596
// Relative URLs should always be specified without a preceding slash.
597
// Body is sent with Content-Type: application/x-www-form-urlencoded.
598
func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
599
if !strings.HasSuffix(c.BaseURL.Path, "/") {
600
return nil, fmt.Errorf("baseURL must have a trailing slash, but %q does not", c.BaseURL)
601
}
602
603
u, err := c.BaseURL.Parse(urlStr)
604
if err != nil {
605
return nil, err
606
}
607
608
req, err := http.NewRequest("POST", u.String(), body)
609
if err != nil {
610
return nil, err
611
}
612
613
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
614
req.Header.Set("Accept", mediaTypeV3)
615
if c.UserAgent != "" {
616
req.Header.Set("User-Agent", c.UserAgent)
617
}
618
req.Header.Set(headerAPIVersion, defaultAPIVersion)
619
620
for _, opt := range opts {
621
opt(req)
622
}
623
624
return req, nil
625
}
626
627
// NewUploadRequest creates an upload request. A relative URL can be provided in
628
// urlStr, in which case it is resolved relative to the UploadURL of the Client.
629
// Relative URLs should always be specified without a preceding slash.
630
func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) {
631
if !strings.HasSuffix(c.UploadURL.Path, "/") {
632
return nil, fmt.Errorf("uploadURL must have a trailing slash, but %q does not", c.UploadURL)
633
}
634
u, err := c.UploadURL.Parse(urlStr)
635
if err != nil {
636
return nil, err
637
}
638
639
requestBody := reader
640
if reader != nil {
641
// Wrap the provided reader so transport code does not observe concrete body types
642
// (for example *os.File) and switch to platform-specific sendfile fast paths.
643
//
644
// Why this exists:
645
// race-enabled test runs on Windows have surfaced data races in the sendfile path
646
// while request read/write loops run concurrently. Hiding concrete type information
647
// keeps uploads on the generic io.Reader copy path, which is race-stable and preserves
648
// request semantics (same bytes, same headers, same content length).
649
requestBody = uploadRequestBodyReader{Reader: reader}
650
}
651
652
req, err := http.NewRequest("POST", u.String(), requestBody)
653
if err != nil {
654
return nil, err
655
}
656
657
req.ContentLength = size
658
659
if mediaType == "" {
660
mediaType = defaultMediaType
661
}
662
req.Header.Set("Content-Type", mediaType)
663
req.Header.Set("Accept", mediaTypeV3)
664
req.Header.Set("User-Agent", c.UserAgent)
665
req.Header.Set(headerAPIVersion, defaultAPIVersion)
666
667
for _, opt := range opts {
668
opt(req)
669
}
670
671
return req, nil
672
}
673
674
// uploadRequestBodyReader intentionally wraps an io.Reader to hide concrete reader types.
675
// See NewUploadRequest for why this prevents race-prone transport optimizations.
676
type uploadRequestBodyReader struct {
677
io.Reader
678
}
679
680
// Response is a GitHub API response. This wraps the standard http.Response
681
// returned from GitHub and provides convenient access to things like
682
// pagination links.
683
type Response struct {
684
*http.Response
685
686
// These fields provide the page values for paginating through a set of
687
// results. Any or all of these may be set to the zero value for
688
// responses that are not part of a paginated set, or for which there
689
// are no additional pages.
690
//
691
// These fields support what is called "offset pagination" and should
692
// be used with the ListOptions struct.
693
NextPage int
694
PrevPage int
695
FirstPage int
696
LastPage int
697
698
// Additionally, some APIs support "cursor pagination" instead of offset.
699
// This means that a token points directly to the next record which
700
// can lead to O(1) performance compared to O(n) performance provided
701
// by offset pagination.
702
//
703
// For APIs that support cursor pagination (such as
704
// TeamsService.ListIDPGroupsInOrganization), the following field
705
// will be populated to point to the next page.
706
//
707
// To use this token, set ListCursorOptions.Page to this value before
708
// calling the endpoint again.
709
NextPageToken string
710
711
// For APIs that support cursor pagination, such as RepositoriesService.ListHookDeliveries,
712
// the following field will be populated to point to the next page.
713
// Set ListCursorOptions.Cursor to this value when calling the endpoint again.
714
Cursor string
715
716
// For APIs that support before/after pagination, such as OrganizationsService.AuditLog.
717
Before string
718
After string
719
720
// Explicitly specify the Rate type so Rate's String() receiver doesn't
721
// propagate to Response.
722
Rate Rate
723
724
// token's expiration date. Timestamp is 0001-01-01 when token doesn't expire.
725
// So it is valid for TokenExpiration.Equal(Timestamp{}) or TokenExpiration.Time.After(time.Now())
726
TokenExpiration Timestamp
727
}
728
729
// newResponse creates a new Response for the provided http.Response.
730
// r must not be nil.
731
func newResponse(r *http.Response) *Response {
732
response := &Response{Response: r}
733
response.populatePageValues()
734
response.Rate = parseRate(r)
735
response.TokenExpiration = parseTokenExpiration(r)
736
return response
737
}
738
739
// populatePageValues parses the HTTP Link response headers and populates the
740
// various pagination link values in the Response.
741
func (r *Response) populatePageValues() {
742
if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
743
for link := range strings.SplitSeq(links[0], ",") {
744
segments := strings.Split(strings.TrimSpace(link), ";")
745
746
// link must at least have href and rel
747
if len(segments) < 2 {
748
continue
749
}
750
751
// ensure href is properly formatted
752
if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
753
continue
754
}
755
756
// try to pull out page parameter
757
url, err := url.Parse(segments[0][1 : len(segments[0])-1])
758
if err != nil {
759
continue
760
}
761
762
q := url.Query()
763
764
if cursor := q.Get("cursor"); cursor != "" {
765
for _, segment := range segments[1:] {
766
if strings.TrimSpace(segment) == `rel="next"` {
767
r.Cursor = cursor
768
}
769
}
770
771
continue
772
}
773
774
page := q.Get("page")
775
since := q.Get("since")
776
before := q.Get("before")
777
after := q.Get("after")
778
779
if page == "" && before == "" && after == "" && since == "" {
780
continue
781
}
782
783
if since != "" && page == "" {
784
page = since
785
}
786
787
for _, segment := range segments[1:] {
788
switch strings.TrimSpace(segment) {
789
case `rel="next"`:
790
if r.NextPage, err = strconv.Atoi(page); err != nil {
791
r.NextPageToken = page
792
}
793
r.After = after
794
case `rel="prev"`:
795
r.PrevPage, _ = strconv.Atoi(page)
796
r.Before = before
797
case `rel="first"`:
798
r.FirstPage, _ = strconv.Atoi(page)
799
case `rel="last"`:
800
r.LastPage, _ = strconv.Atoi(page)
801
}
802
}
803
}
804
}
805
}
806
807
// parseRate parses the rate related headers.
808
func parseRate(r *http.Response) Rate {
809
var rate Rate
810
if limit := r.Header.Get(HeaderRateLimit); limit != "" {
811
rate.Limit, _ = strconv.Atoi(limit)
812
}
813
if remaining := r.Header.Get(HeaderRateRemaining); remaining != "" {
814
rate.Remaining, _ = strconv.Atoi(remaining)
815
}
816
if used := r.Header.Get(HeaderRateUsed); used != "" {
817
rate.Used, _ = strconv.Atoi(used)
818
}
819
if reset := r.Header.Get(HeaderRateReset); reset != "" {
820
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
821
rate.Reset = Timestamp{time.Unix(v, 0)}
822
}
823
}
824
if resource := r.Header.Get(HeaderRateResource); resource != "" {
825
rate.Resource = resource
826
}
827
return rate
828
}
829
830
// parseSecondaryRate parses the secondary rate related headers,
831
// and returns the time to retry after.
832
func parseSecondaryRate(r *http.Response) *time.Duration {
833
// According to GitHub support, the "Retry-After" header value will be
834
// an integer which represents the number of seconds that one should
835
// wait before resuming making requests.
836
if v := r.Header.Get(headerRetryAfter); v != "" {
837
retryAfterSeconds, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop.
838
retryAfter := time.Duration(retryAfterSeconds) * time.Second
839
return &retryAfter
840
}
841
842
// According to GitHub support, endpoints might return x-ratelimit-reset instead,
843
// as an integer which represents the number of seconds since epoch UTC,
844
// representing the time to resume making requests.
845
if v := r.Header.Get(HeaderRateReset); v != "" {
846
secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64) // Error handling is noop.
847
retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0))
848
return &retryAfter
849
}
850
851
return nil
852
}
853
854
// parseTokenExpiration parses the TokenExpiration related headers.
855
// Returns 0001-01-01 if the header is not defined or could not be parsed.
856
func parseTokenExpiration(r *http.Response) Timestamp {
857
if v := r.Header.Get(headerTokenExpiration); v != "" {
858
if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil {
859
return Timestamp{t.Local()}
860
}
861
// Some tokens include the timezone offset instead of the timezone.
862
// https://github.com/google/go-github/issues/2649
863
if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil {
864
return Timestamp{t.Local()}
865
}
866
}
867
return Timestamp{} // 0001-01-01 00:00:00
868
}
869
870
type requestContext uint8
871
872
const (
873
// BypassRateLimitCheck prevents a pre-emptive check for exceeded primary rate limits
874
// Specify this by providing a context with this key, e.g.
875
// context.WithValue(context.Background(), github.BypassRateLimitCheck, true)
876
BypassRateLimitCheck requestContext = iota
877
878
SleepUntilPrimaryRateLimitResetWhenRateLimited
879
)
880
881
// bareDo sends an API request using `caller` http.Client passed in the parameters
882
// and lets you handle the api response. If an error or API Error occurs, the error
883
// will contain more information. Otherwise, you are supposed to read and close the
884
// response's Body. If rate limit is exceeded and reset time is in the future,
885
// bareDo returns *RateLimitError immediately without making a network API call.
886
//
887
// The provided ctx must be non-nil, if it is nil an error is returned. If it is
888
// canceled or times out, ctx.Err() will be returned.
889
func (c *Client) bareDo(ctx context.Context, caller *http.Client, req *http.Request) (*Response, error) {
890
if ctx == nil {
891
return nil, errNonNilContext
892
}
893
894
req = withContext(ctx, req)
895
896
rateLimitCategory := CoreCategory
897
898
if !c.DisableRateLimitCheck {
899
rateLimitCategory = GetRateLimitCategory(req.Method, req.URL.Path)
900
901
if bypass := ctx.Value(BypassRateLimitCheck); bypass == nil {
902
// If we've hit rate limit, don't make further requests before Reset time.
903
if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
904
return &Response{
905
Response: err.Response,
906
Rate: err.Rate,
907
}, err
908
}
909
910
// If we've hit a secondary rate limit, don't make further requests before Retry After.
911
if err := c.checkSecondaryRateLimitBeforeDo(req); err != nil {
912
return &Response{
913
Response: err.Response,
914
}, err
915
}
916
}
917
}
918
919
resp, err := caller.Do(req)
920
var response *Response
921
if resp != nil {
922
response = newResponse(resp)
923
}
924
925
if err != nil {
926
// If we got an error, and the context has been canceled,
927
// the context's error is probably more useful.
928
select {
929
case <-ctx.Done():
930
return response, ctx.Err()
931
default:
932
}
933
934
// If the error type is *url.Error, sanitize its URL before returning.
935
var e *url.Error
936
if errors.As(err, &e) {
937
if url, err := url.Parse(e.URL); err == nil {
938
e.URL = sanitizeURL(url).String()
939
return response, e
940
}
941
}
942
943
return response, err
944
}
945
946
// Don't update the rate limits if the client has rate limits disabled or if
947
// this was a cached response. The X-From-Cache is set by
948
// https://github.com/bartventer/httpcache if it's enabled.
949
if !c.DisableRateLimitCheck && response.Header.Get("X-From-Cache") == "" {
950
c.rateMu.Lock()
951
c.rateLimits[rateLimitCategory] = response.Rate
952
c.rateMu.Unlock()
953
}
954
955
err = CheckResponse(resp)
956
if err != nil {
957
defer resp.Body.Close()
958
// Special case for AcceptedErrors. If an AcceptedError
959
// has been encountered, the response's payload will be
960
// added to the AcceptedError and returned.
961
//
962
// Issue #1022
963
var aerr *AcceptedError
964
if errors.As(err, &aerr) {
965
b, readErr := io.ReadAll(resp.Body)
966
if readErr != nil {
967
return response, readErr
968
}
969
970
aerr.Raw = b
971
err = aerr
972
}
973
974
var rateLimitError *RateLimitError
975
if errors.As(err, &rateLimitError) &&
976
req.Context().Value(SleepUntilPrimaryRateLimitResetWhenRateLimited) != nil {
977
if err := sleepUntilResetWithBuffer(req.Context(), rateLimitError.Rate.Reset.Time); err != nil {
978
return response, err
979
}
980
// retry the request once when the rate limit has reset
981
return c.bareDo(context.WithValue(req.Context(), SleepUntilPrimaryRateLimitResetWhenRateLimited, nil), caller, req)
982
}
983
984
// Update the secondary rate limit if we hit it.
985
var rerr *AbuseRateLimitError
986
if errors.As(err, &rerr) && rerr.RetryAfter != nil {
987
// if a max duration is specified, make sure that we are waiting at most this duration
988
if c.MaxSecondaryRateLimitRetryAfterDuration > 0 && rerr.GetRetryAfter() > c.MaxSecondaryRateLimitRetryAfterDuration {
989
rerr.RetryAfter = &c.MaxSecondaryRateLimitRetryAfterDuration
990
}
991
c.rateMu.Lock()
992
c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter)
993
c.rateMu.Unlock()
994
}
995
}
996
return response, err
997
}
998
999
// BareDo sends an API request and lets you handle the api response. If an error
1000
// or API Error occurs, the error will contain more information. Otherwise, you