1
2
3
4
5
6
7
8
9 package github
10
11 import (
12 "bytes"
13 "context"
14 "encoding/json"
15 "errors"
16 "fmt"
17 "io"
18 "net/http"
19 "net/url"
20 "reflect"
21 "strconv"
22 "strings"
23 "sync"
24 "time"
25
26 "github.com/google/go-querystring/query"
27 )
28
29 const (
30 Version = "v55.0.0"
31
32 defaultAPIVersion = "2022-11-28"
33 defaultBaseURL = "https://api.github.com/"
34 defaultUserAgent = "go-github" + "/" + Version
35 uploadBaseURL = "https://uploads.github.com/"
36
37 headerAPIVersion = "X-GitHub-Api-Version"
38 headerRateLimit = "X-RateLimit-Limit"
39 headerRateRemaining = "X-RateLimit-Remaining"
40 headerRateReset = "X-RateLimit-Reset"
41 headerOTP = "X-GitHub-OTP"
42 headerRetryAfter = "Retry-After"
43
44 headerTokenExpiration = "GitHub-Authentication-Token-Expiration"
45
46 mediaTypeV3 = "application/vnd.github.v3+json"
47 defaultMediaType = "application/octet-stream"
48 mediaTypeV3SHA = "application/vnd.github.v3.sha"
49 mediaTypeV3Diff = "application/vnd.github.v3.diff"
50 mediaTypeV3Patch = "application/vnd.github.v3.patch"
51 mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
52 mediaTypeIssueImportAPI = "application/vnd.github.golden-comet-preview+json"
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72 mediaTypeStarringPreview = "application/vnd.github.v3.star+json"
73
74
75 mediaTypeMigrationsPreview = "application/vnd.github.wyandotte-preview+json"
76
77
78 mediaTypeDeploymentStatusPreview = "application/vnd.github.ant-man-preview+json"
79
80
81 mediaTypeExpandDeploymentStatusPreview = "application/vnd.github.flash-preview+json"
82
83
84 mediaTypeReactionsPreview = "application/vnd.github.squirrel-girl-preview"
85
86
87 mediaTypeTimelinePreview = "application/vnd.github.mockingbird-preview+json"
88
89
90 mediaTypeProjectsPreview = "application/vnd.github.inertia-preview+json"
91
92
93 mediaTypeCommitSearchPreview = "application/vnd.github.cloak-preview+json"
94
95
96 mediaTypeBlockUsersPreview = "application/vnd.github.giant-sentry-fist-preview+json"
97
98
99 mediaTypeCodesOfConductPreview = "application/vnd.github.scarlet-witch-preview+json"
100
101
102 mediaTypeTopicsPreview = "application/vnd.github.mercy-preview+json"
103
104
105 mediaTypeRequiredApprovingReviewsPreview = "application/vnd.github.luke-cage-preview+json"
106
107
108 mediaTypeCheckRunsPreview = "application/vnd.github.antiope-preview+json"
109
110
111 mediaTypePreReceiveHooksPreview = "application/vnd.github.eye-scream-preview"
112
113
114 mediaTypeSignaturePreview = "application/vnd.github.zzzax-preview+json"
115
116
117 mediaTypeProjectCardDetailsPreview = "application/vnd.github.starfox-preview+json"
118
119
120 mediaTypeInteractionRestrictionsPreview = "application/vnd.github.sombra-preview+json"
121
122
123 mediaTypeEnablePagesAPIPreview = "application/vnd.github.switcheroo-preview+json"
124
125
126 mediaTypeRequiredVulnerabilityAlertsPreview = "application/vnd.github.dorian-preview+json"
127
128
129 mediaTypeUpdatePullRequestBranchPreview = "application/vnd.github.lydian-preview+json"
130
131
132 mediaTypeListPullsOrBranchesForCommitPreview = "application/vnd.github.groot-preview+json"
133
134
135 mediaTypeMemberAllowedRepoCreationTypePreview = "application/vnd.github.surtur-preview+json"
136
137
138 mediaTypeRepositoryTemplatePreview = "application/vnd.github.baptiste-preview+json"
139
140
141 mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
142
143
144 mediaTypeOAuthAppPreview = "application/vnd.github.doctor-strange-preview+json"
145
146
147 mediaTypeRepositoryVisibilityPreview = "application/vnd.github.nebula-preview+json"
148
149
150 mediaTypeContentAttachmentsPreview = "application/vnd.github.corsair-preview+json"
151 )
152
153 var errNonNilContext = errors.New("context must be non-nil")
154
155
156 type Client struct {
157 clientMu sync.Mutex
158 client *http.Client
159
160
161
162
163 BaseURL *url.URL
164
165
166 UploadURL *url.URL
167
168
169 UserAgent string
170
171 rateMu sync.Mutex
172 rateLimits [categories]Rate
173 secondaryRateLimitReset time.Time
174
175 common service
176
177
178 Actions *ActionsService
179 Activity *ActivityService
180 Admin *AdminService
181 Apps *AppsService
182 Authorizations *AuthorizationsService
183 Billing *BillingService
184 Checks *ChecksService
185 CodeScanning *CodeScanningService
186 Codespaces *CodespacesService
187 Dependabot *DependabotService
188 DependencyGraph *DependencyGraphService
189 Enterprise *EnterpriseService
190 Gists *GistsService
191 Git *GitService
192 Gitignores *GitignoresService
193 Interactions *InteractionsService
194 IssueImport *IssueImportService
195 Issues *IssuesService
196 Licenses *LicensesService
197 Marketplace *MarketplaceService
198 Migrations *MigrationService
199 Organizations *OrganizationsService
200 Projects *ProjectsService
201 PullRequests *PullRequestsService
202 Reactions *ReactionsService
203 Repositories *RepositoriesService
204 SCIM *SCIMService
205 Search *SearchService
206 SecretScanning *SecretScanningService
207 SecurityAdvisories *SecurityAdvisoriesService
208 Teams *TeamsService
209 Users *UsersService
210 }
211
212 type service struct {
213 client *Client
214 }
215
216
217 func (c *Client) Client() *http.Client {
218 c.clientMu.Lock()
219 defer c.clientMu.Unlock()
220 clientCopy := *c.client
221 return &clientCopy
222 }
223
224
225
226 type ListOptions struct {
227
228 Page int `url:"page,omitempty"`
229
230
231 PerPage int `url:"per_page,omitempty"`
232 }
233
234
235
236 type ListCursorOptions struct {
237
238 Page string `url:"page,omitempty"`
239
240
241 PerPage int `url:"per_page,omitempty"`
242
243
244
245 First int `url:"first,omitempty"`
246
247
248
249 Last int `url:"last,omitempty"`
250
251
252 After string `url:"after,omitempty"`
253
254
255 Before string `url:"before,omitempty"`
256
257
258 Cursor string `url:"cursor,omitempty"`
259 }
260
261
262 type UploadOptions struct {
263 Name string `url:"name,omitempty"`
264 Label string `url:"label,omitempty"`
265 MediaType string `url:"-"`
266 }
267
268
269 type RawType uint8
270
271 const (
272
273 Diff RawType = 1 + iota
274
275 Patch
276 )
277
278
279
280 type RawOptions struct {
281 Type RawType
282 }
283
284
285
286 func addOptions(s string, opts interface{}) (string, error) {
287 v := reflect.ValueOf(opts)
288 if v.Kind() == reflect.Ptr && v.IsNil() {
289 return s, nil
290 }
291
292 u, err := url.Parse(s)
293 if err != nil {
294 return s, err
295 }
296
297 qs, err := query.Values(opts)
298 if err != nil {
299 return s, err
300 }
301
302 u.RawQuery = qs.Encode()
303 return u.String(), nil
304 }
305
306
307
308
309
310
311 func NewClient(httpClient *http.Client) *Client {
312 c := &Client{client: httpClient}
313 c.initialize()
314 return c
315 }
316
317
318 func (c *Client) WithAuthToken(token string) *Client {
319 c2 := c.copy()
320 defer c2.initialize()
321 transport := c2.client.Transport
322 if transport == nil {
323 transport = http.DefaultTransport
324 }
325 c2.client.Transport = roundTripperFunc(
326 func(req *http.Request) (*http.Response, error) {
327 req = req.Clone(req.Context())
328 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
329 return transport.RoundTrip(req)
330 },
331 )
332 return c2
333 }
334
335
336
337
338
339
340
341
342
343
344
345
346 func (c *Client) WithEnterpriseURLs(baseURL, uploadURL string) (*Client, error) {
347 c2 := c.copy()
348 defer c2.initialize()
349 var err error
350 c2.BaseURL, err = url.Parse(baseURL)
351 if err != nil {
352 return nil, err
353 }
354
355 if !strings.HasSuffix(c2.BaseURL.Path, "/") {
356 c2.BaseURL.Path += "/"
357 }
358 if !strings.HasSuffix(c2.BaseURL.Path, "/api/v3/") &&
359 !strings.HasPrefix(c2.BaseURL.Host, "api.") &&
360 !strings.Contains(c2.BaseURL.Host, ".api.") {
361 c2.BaseURL.Path += "api/v3/"
362 }
363
364 c2.UploadURL, err = url.Parse(uploadURL)
365 if err != nil {
366 return nil, err
367 }
368
369 if !strings.HasSuffix(c2.UploadURL.Path, "/") {
370 c2.UploadURL.Path += "/"
371 }
372 if !strings.HasSuffix(c2.UploadURL.Path, "/api/uploads/") &&
373 !strings.HasPrefix(c2.UploadURL.Host, "api.") &&
374 !strings.Contains(c2.UploadURL.Host, ".api.") {
375 c2.UploadURL.Path += "api/uploads/"
376 }
377 return c2, nil
378 }
379
380
381 func (c *Client) initialize() {
382 if c.client == nil {
383 c.client = &http.Client{}
384 }
385 if c.BaseURL == nil {
386 c.BaseURL, _ = url.Parse(defaultBaseURL)
387 }
388 if c.UploadURL == nil {
389 c.UploadURL, _ = url.Parse(uploadBaseURL)
390 }
391 if c.UserAgent == "" {
392 c.UserAgent = defaultUserAgent
393 }
394 c.common.client = c
395 c.Actions = (*ActionsService)(&c.common)
396 c.Activity = (*ActivityService)(&c.common)
397 c.Admin = (*AdminService)(&c.common)
398 c.Apps = (*AppsService)(&c.common)
399 c.Authorizations = (*AuthorizationsService)(&c.common)
400 c.Billing = (*BillingService)(&c.common)
401 c.Checks = (*ChecksService)(&c.common)
402 c.CodeScanning = (*CodeScanningService)(&c.common)
403 c.Codespaces = (*CodespacesService)(&c.common)
404 c.Dependabot = (*DependabotService)(&c.common)
405 c.DependencyGraph = (*DependencyGraphService)(&c.common)
406 c.Enterprise = (*EnterpriseService)(&c.common)
407 c.Gists = (*GistsService)(&c.common)
408 c.Git = (*GitService)(&c.common)
409 c.Gitignores = (*GitignoresService)(&c.common)
410 c.Interactions = (*InteractionsService)(&c.common)
411 c.IssueImport = (*IssueImportService)(&c.common)
412 c.Issues = (*IssuesService)(&c.common)
413 c.Licenses = (*LicensesService)(&c.common)
414 c.Marketplace = &MarketplaceService{client: c}
415 c.Migrations = (*MigrationService)(&c.common)
416 c.Organizations = (*OrganizationsService)(&c.common)
417 c.Projects = (*ProjectsService)(&c.common)
418 c.PullRequests = (*PullRequestsService)(&c.common)
419 c.Reactions = (*ReactionsService)(&c.common)
420 c.Repositories = (*RepositoriesService)(&c.common)
421 c.SCIM = (*SCIMService)(&c.common)
422 c.Search = (*SearchService)(&c.common)
423 c.SecretScanning = (*SecretScanningService)(&c.common)
424 c.SecurityAdvisories = (*SecurityAdvisoriesService)(&c.common)
425 c.Teams = (*TeamsService)(&c.common)
426 c.Users = (*UsersService)(&c.common)
427 }
428
429
430 func (c *Client) copy() *Client {
431 c.clientMu.Lock()
432
433 clone := Client{
434 client: c.client,
435 UserAgent: c.UserAgent,
436 BaseURL: c.BaseURL,
437 UploadURL: c.UploadURL,
438 secondaryRateLimitReset: c.secondaryRateLimitReset,
439 }
440 c.clientMu.Unlock()
441 if clone.client == nil {
442 clone.client = &http.Client{}
443 }
444 c.rateMu.Lock()
445 copy(clone.rateLimits[:], c.rateLimits[:])
446 c.rateMu.Unlock()
447 return &clone
448 }
449
450
451 func NewClientWithEnvProxy() *Client {
452 return NewClient(&http.Client{Transport: &http.Transport{Proxy: http.ProxyFromEnvironment}})
453 }
454
455
456
457 func NewTokenClient(_ context.Context, token string) *Client {
458
459 return NewClient(nil).WithAuthToken(token)
460 }
461
462
463
464
465
466 func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
467 return NewClient(httpClient).WithEnterpriseURLs(baseURL, uploadURL)
468 }
469
470
471 type RequestOption func(req *http.Request)
472
473
474
475
476 func WithVersion(version string) RequestOption {
477 return func(req *http.Request) {
478 req.Header.Set(headerAPIVersion, version)
479 }
480 }
481
482
483
484
485
486
487 func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) {
488 if !strings.HasSuffix(c.BaseURL.Path, "/") {
489 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
490 }
491
492 u, err := c.BaseURL.Parse(urlStr)
493 if err != nil {
494 return nil, err
495 }
496
497 var buf io.ReadWriter
498 if body != nil {
499 buf = &bytes.Buffer{}
500 enc := json.NewEncoder(buf)
501 enc.SetEscapeHTML(false)
502 err := enc.Encode(body)
503 if err != nil {
504 return nil, err
505 }
506 }
507
508 req, err := http.NewRequest(method, u.String(), buf)
509 if err != nil {
510 return nil, err
511 }
512
513 if body != nil {
514 req.Header.Set("Content-Type", "application/json")
515 }
516 req.Header.Set("Accept", mediaTypeV3)
517 if c.UserAgent != "" {
518 req.Header.Set("User-Agent", c.UserAgent)
519 }
520 req.Header.Set(headerAPIVersion, defaultAPIVersion)
521
522 for _, opt := range opts {
523 opt(req)
524 }
525
526 return req, nil
527 }
528
529
530
531
532
533 func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) {
534 if !strings.HasSuffix(c.BaseURL.Path, "/") {
535 return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL)
536 }
537
538 u, err := c.BaseURL.Parse(urlStr)
539 if err != nil {
540 return nil, err
541 }
542
543 req, err := http.NewRequest(http.MethodPost, u.String(), body)
544 if err != nil {
545 return nil, err
546 }
547
548 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
549 req.Header.Set("Accept", mediaTypeV3)
550 if c.UserAgent != "" {
551 req.Header.Set("User-Agent", c.UserAgent)
552 }
553 req.Header.Set(headerAPIVersion, defaultAPIVersion)
554
555 for _, opt := range opts {
556 opt(req)
557 }
558
559 return req, nil
560 }
561
562
563
564
565 func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) {
566 if !strings.HasSuffix(c.UploadURL.Path, "/") {
567 return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL)
568 }
569 u, err := c.UploadURL.Parse(urlStr)
570 if err != nil {
571 return nil, err
572 }
573
574 req, err := http.NewRequest("POST", u.String(), reader)
575 if err != nil {
576 return nil, err
577 }
578
579 req.ContentLength = size
580
581 if mediaType == "" {
582 mediaType = defaultMediaType
583 }
584 req.Header.Set("Content-Type", mediaType)
585 req.Header.Set("Accept", mediaTypeV3)
586 req.Header.Set("User-Agent", c.UserAgent)
587 req.Header.Set(headerAPIVersion, defaultAPIVersion)
588
589 for _, opt := range opts {
590 opt(req)
591 }
592
593 return req, nil
594 }
595
596
597
598
599 type Response struct {
600 *http.Response
601
602
603
604
605
606
607
608
609 NextPage int
610 PrevPage int
611 FirstPage int
612 LastPage int
613
614
615
616
617
618
619
620
621
622
623
624
625 NextPageToken string
626
627
628
629
630 Cursor string
631
632
633 Before string
634 After string
635
636
637
638 Rate Rate
639
640
641
642 TokenExpiration Timestamp
643 }
644
645
646
647 func newResponse(r *http.Response) *Response {
648 response := &Response{Response: r}
649 response.populatePageValues()
650 response.Rate = parseRate(r)
651 response.TokenExpiration = parseTokenExpiration(r)
652 return response
653 }
654
655
656
657 func (r *Response) populatePageValues() {
658 if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 {
659 for _, link := range strings.Split(links[0], ",") {
660 segments := strings.Split(strings.TrimSpace(link), ";")
661
662
663 if len(segments) < 2 {
664 continue
665 }
666
667
668 if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") {
669 continue
670 }
671
672
673 url, err := url.Parse(segments[0][1 : len(segments[0])-1])
674 if err != nil {
675 continue
676 }
677
678 q := url.Query()
679
680 if cursor := q.Get("cursor"); cursor != "" {
681 for _, segment := range segments[1:] {
682 switch strings.TrimSpace(segment) {
683 case `rel="next"`:
684 r.Cursor = cursor
685 }
686 }
687
688 continue
689 }
690
691 page := q.Get("page")
692 since := q.Get("since")
693 before := q.Get("before")
694 after := q.Get("after")
695
696 if page == "" && before == "" && after == "" && since == "" {
697 continue
698 }
699
700 if since != "" && page == "" {
701 page = since
702 }
703
704 for _, segment := range segments[1:] {
705 switch strings.TrimSpace(segment) {
706 case `rel="next"`:
707 if r.NextPage, err = strconv.Atoi(page); err != nil {
708 r.NextPageToken = page
709 }
710 r.After = after
711 case `rel="prev"`:
712 r.PrevPage, _ = strconv.Atoi(page)
713 r.Before = before
714 case `rel="first"`:
715 r.FirstPage, _ = strconv.Atoi(page)
716 case `rel="last"`:
717 r.LastPage, _ = strconv.Atoi(page)
718 }
719 }
720 }
721 }
722 }
723
724
725 func parseRate(r *http.Response) Rate {
726 var rate Rate
727 if limit := r.Header.Get(headerRateLimit); limit != "" {
728 rate.Limit, _ = strconv.Atoi(limit)
729 }
730 if remaining := r.Header.Get(headerRateRemaining); remaining != "" {
731 rate.Remaining, _ = strconv.Atoi(remaining)
732 }
733 if reset := r.Header.Get(headerRateReset); reset != "" {
734 if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
735 rate.Reset = Timestamp{time.Unix(v, 0)}
736 }
737 }
738 return rate
739 }
740
741
742
743 func parseSecondaryRate(r *http.Response) *time.Duration {
744
745
746
747 if v := r.Header.Get(headerRetryAfter); v != "" {
748 retryAfterSeconds, _ := strconv.ParseInt(v, 10, 64)
749 retryAfter := time.Duration(retryAfterSeconds) * time.Second
750 return &retryAfter
751 }
752
753
754
755
756 if v := r.Header.Get(headerRateReset); v != "" {
757 secondsSinceEpoch, _ := strconv.ParseInt(v, 10, 64)
758 retryAfter := time.Until(time.Unix(secondsSinceEpoch, 0))
759 return &retryAfter
760 }
761
762 return nil
763 }
764
765
766
767 func parseTokenExpiration(r *http.Response) Timestamp {
768 if v := r.Header.Get(headerTokenExpiration); v != "" {
769 if t, err := time.Parse("2006-01-02 15:04:05 MST", v); err == nil {
770 return Timestamp{t.Local()}
771 }
772
773
774 if t, err := time.Parse("2006-01-02 15:04:05 -0700", v); err == nil {
775 return Timestamp{t.Local()}
776 }
777 }
778 return Timestamp{}
779 }
780
781 type requestContext uint8
782
783 const (
784 bypassRateLimitCheck requestContext = iota
785 )
786
787
788
789
790
791
792
793
794
795 func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, error) {
796 if ctx == nil {
797 return nil, errNonNilContext
798 }
799
800 req = withContext(ctx, req)
801
802 rateLimitCategory := category(req.Method, req.URL.Path)
803
804 if bypass := ctx.Value(bypassRateLimitCheck); bypass == nil {
805
806 if err := c.checkRateLimitBeforeDo(req, rateLimitCategory); err != nil {
807 return &Response{
808 Response: err.Response,
809 Rate: err.Rate,
810 }, err
811 }
812
813 if err := c.checkSecondaryRateLimitBeforeDo(ctx, req); err != nil {
814 return &Response{
815 Response: err.Response,
816 }, err
817 }
818 }
819
820 resp, err := c.client.Do(req)
821 if err != nil {
822
823
824 select {
825 case <-ctx.Done():
826 return nil, ctx.Err()
827 default:
828 }
829
830
831 if e, ok := err.(*url.Error); ok {
832 if url, err := url.Parse(e.URL); err == nil {
833 e.URL = sanitizeURL(url).String()
834 return nil, e
835 }
836 }
837
838 return nil, err
839 }
840
841 response := newResponse(resp)
842
843
844
845 if response.Header.Get("X-From-Cache") == "" {
846 c.rateMu.Lock()
847 c.rateLimits[rateLimitCategory] = response.Rate
848 c.rateMu.Unlock()
849 }
850
851 err = CheckResponse(resp)
852 if err != nil {
853 defer resp.Body.Close()
854
855
856
857
858
859 aerr, ok := err.(*AcceptedError)
860 if ok {
861 b, readErr := io.ReadAll(resp.Body)
862 if readErr != nil {
863 return response, readErr
864 }
865
866 aerr.Raw = b
867 err = aerr
868 }
869
870
871 rerr, ok := err.(*AbuseRateLimitError)
872 if ok && rerr.RetryAfter != nil {
873 c.rateMu.Lock()
874 c.secondaryRateLimitReset = time.Now().Add(*rerr.RetryAfter)
875 c.rateMu.Unlock()
876 }
877 }
878 return response, err
879 }
880
881
882
883
884
885
886
887
888
889
890
891 func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
892 resp, err := c.BareDo(ctx, req)
893 if err != nil {
894 return resp, err
895 }
896 defer resp.Body.Close()
897
898 switch v := v.(type) {
899 case nil:
900 case io.Writer:
901 _, err = io.Copy(v, resp.Body)
902 default:
903 decErr := json.NewDecoder(resp.Body).Decode(v)
904 if decErr == io.EOF {
905 decErr = nil
906 }
907 if decErr != nil {
908 err = decErr
909 }
910 }
911 return resp, err
912 }
913
914
915
916
917
918 func (c *Client) checkRateLimitBeforeDo(req *http.Request, rateLimitCategory rateLimitCategory) *RateLimitError {
919 c.rateMu.Lock()
920 rate := c.rateLimits[rateLimitCategory]
921 c.rateMu.Unlock()
922 if !rate.Reset.Time.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset.Time) {
923
924 resp := &http.Response{
925 Status: http.StatusText(http.StatusForbidden),
926 StatusCode: http.StatusForbidden,
927 Request: req,
928 Header: make(http.Header),
929 Body: io.NopCloser(strings.NewReader("")),
930 }
931 return &RateLimitError{
932 Rate: rate,
933 Response: resp,
934 Message: fmt.Sprintf("API rate limit of %v still exceeded until %v, not making remote request.", rate.Limit, rate.Reset.Time),
935 }
936 }
937
938 return nil
939 }
940
941
942
943
944
945 func (c *Client) checkSecondaryRateLimitBeforeDo(ctx context.Context, req *http.Request) *AbuseRateLimitError {
946 c.rateMu.Lock()
947 secondary := c.secondaryRateLimitReset
948 c.rateMu.Unlock()
949 if !secondary.IsZero() && time.Now().Before(secondary) {
950
951 resp := &http.Response{
952 Status: http.StatusText(http.StatusForbidden),
953 StatusCode: http.StatusForbidden,
954 Request: req,
955 Header: make(http.Header),
956 Body: io.NopCloser(strings.NewReader("")),
957 }
958
959 retryAfter := time.Until(secondary)
960 return &AbuseRateLimitError{
961 Response: resp,
962 Message: fmt.Sprintf("API secondary rate limit exceeded until %v, not making remote request.", secondary),
963 RetryAfter: &retryAfter,
964 }
965 }
966
967 return nil
968 }
969
970
971
972
973 func compareHTTPResponse(r1, r2 *http.Response) bool {
974 if r1 == nil && r2 == nil {
975 return true
976 }
977
978 if r1 != nil && r2 != nil {
979 return r1.StatusCode == r2.StatusCode
980 }
981 return false
982 }
983
984
989 type ErrorResponse struct {
990 Response *http.Response `json:"-"`
991 Message string `json:"message"`
992 Errors []Error `json:"errors"`
993
994 Block *ErrorBlock `json:"block,omitempty"`
995
996
997
998 DocumentationURL string `json:"documentation_url,omitempty"`
999 }
1000
1001
1002
1003
1004 type ErrorBlock struct {
1005 Reason string `json:"reason,omitempty"`
1006 CreatedAt *Timestamp `json:"created_at,omitempty"`
1007 }
1008
1009 func (r *ErrorResponse) Error() string {
1010 return fmt.Sprintf("%v %v: %d %v %+v",
1011 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
1012 r.Response.StatusCode, r.Message, r.Errors)
1013 }
1014
1015
1016 func (r *ErrorResponse) Is(target error) bool {
1017 v, ok := target.(*ErrorResponse)
1018 if !ok {
1019 return false
1020 }
1021
1022 if r.Message != v.Message || (r.DocumentationURL != v.DocumentationURL) ||
1023 !compareHTTPResponse(r.Response, v.Response) {
1024 return false
1025 }
1026
1027
1028 if len(r.Errors) != len(v.Errors) {
1029 return false
1030 }
1031 for idx := range r.Errors {
1032 if r.Errors[idx] != v.Errors[idx] {
1033 return false
1034 }
1035 }
1036
1037
1038 if (r.Block != nil && v.Block == nil) || (r.Block == nil && v.Block != nil) {
1039 return false
1040 }
1041 if r.Block != nil && v.Block != nil {
1042 if r.Block.Reason != v.Block.Reason {
1043 return false
1044 }
1045 if (r.Block.CreatedAt != nil && v.Block.CreatedAt == nil) || (r.Block.CreatedAt ==
1046 nil && v.Block.CreatedAt != nil) {
1047 return false
1048 }
1049 if r.Block.CreatedAt != nil && v.Block.CreatedAt != nil {
1050 if *(r.Block.CreatedAt) != *(v.Block.CreatedAt) {
1051 return false
1052 }
1053 }
1054 }
1055
1056 return true
1057 }
1058
1059
1060
1061
1062 type TwoFactorAuthError ErrorResponse
1063
1064 func (r *TwoFactorAuthError) Error() string { return (*ErrorResponse)(r).Error() }
1065
1066
1067
1068 type RateLimitError struct {
1069 Rate Rate
1070 Response *http.Response
1071 Message string `json:"message"`
1072 }
1073
1074 func (r *RateLimitError) Error() string {
1075 return fmt.Sprintf("%v %v: %d %v %v",
1076 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
1077 r.Response.StatusCode, r.Message, formatRateReset(time.Until(r.Rate.Reset.Time)))
1078 }
1079
1080
1081 func (r *RateLimitError) Is(target error) bool {
1082 v, ok := target.(*RateLimitError)
1083 if !ok {
1084 return false
1085 }
1086
1087 return r.Rate == v.Rate &&
1088 r.Message == v.Message &&
1089 compareHTTPResponse(r.Response, v.Response)
1090 }
1091
1092
1093
1094
1095
1096
1097
1098 type AcceptedError struct {
1099
1100 Raw []byte
1101 }
1102
1103 func (*AcceptedError) Error() string {
1104 return "job scheduled on GitHub side; try again later"
1105 }
1106
1107
1108 func (ae *AcceptedError) Is(target error) bool {
1109 v, ok := target.(*AcceptedError)
1110 if !ok {
1111 return false
1112 }
1113 return bytes.Equal(ae.Raw, v.Raw)
1114 }
1115
1116
1117
1118 type AbuseRateLimitError struct {
1119 Response *http.Response
1120 Message string `json:"message"`
1121
1122
1123
1124
1125 RetryAfter *time.Duration
1126 }
1127
1128 func (r *AbuseRateLimitError) Error() string {
1129 return fmt.Sprintf("%v %v: %d %v",
1130 r.Response.Request.Method, sanitizeURL(r.Response.Request.URL),
1131 r.Response.StatusCode, r.Message)
1132 }
1133
1134
1135 func (r *AbuseRateLimitError) Is(target error) bool {
1136 v, ok := target.(*AbuseRateLimitError)
1137 if !ok {
1138 return false
1139 }
1140
1141 return r.Message == v.Message &&
1142 r.RetryAfter == v.RetryAfter &&
1143 compareHTTPResponse(r.Response, v.Response)
1144 }
1145
1146
1147
1148 func sanitizeURL(uri *url.URL) *url.URL {
1149 if uri == nil {
1150 return nil
1151 }
1152 params := uri.Query()
1153 if len(params.Get("client_secret")) > 0 {
1154 params.Set("client_secret", "REDACTED")
1155 uri.RawQuery = params.Encode()
1156 }
1157 return uri
1158 }
1159
1160
1182 type Error struct {
1183 Resource string `json:"resource"`
1184 Field string `json:"field"`
1185 Code string `json:"code"`
1186 Message string `json:"message"`
1187 }
1188
1189 func (e *Error) Error() string {
1190 return fmt.Sprintf("%v error caused by %v field on %v resource",
1191 e.Code, e.Field, e.Resource)
1192 }
1193
1194 func (e *Error) UnmarshalJSON(data []byte) error {
1195 type aliasError Error
1196 if err := json.Unmarshal(data, (*aliasError)(e)); err != nil {
1197 return json.Unmarshal(data, &e.Message)
1198 }
1199 return nil
1200 }
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211 func CheckResponse(r *http.Response) error {
1212 if r.StatusCode == http.StatusAccepted {
1213 return &AcceptedError{}
1214 }
1215 if c := r.StatusCode; 200 <= c && c <= 299 {
1216 return nil
1217 }
1218
1219 errorResponse := &ErrorResponse{Response: r}
1220 data, err := io.ReadAll(r.Body)
1221 if err == nil && data != nil {
1222 json.Unmarshal(data, errorResponse)
1223 }
1224
1225
1226
1227 r.Body = io.NopCloser(bytes.NewBuffer(data))
1228 switch {
1229 case r.StatusCode == http.StatusUnauthorized && strings.HasPrefix(r.Header.Get(headerOTP), "required"):
1230 return (*TwoFactorAuthError)(errorResponse)
1231 case r.StatusCode == http.StatusForbidden && r.Header.Get(headerRateRemaining) == "0":
1232 return &RateLimitError{
1233 Rate: parseRate(r),
1234 Response: errorResponse.Response,
1235 Message: errorResponse.Message,
1236 }
1237 case r.StatusCode == http.StatusForbidden &&
1238 (strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") ||
1239 strings.HasSuffix(errorResponse.DocumentationURL, "#secondary-rate-limits")):
1240 abuseRateLimitError := &AbuseRateLimitError{
1241 Response: errorResponse.Response,
1242 Message: errorResponse.Message,
1243 }
1244 if retryAfter := parseSecondaryRate(r); retryAfter != nil {
1245 abuseRateLimitError.RetryAfter = retryAfter
1246 }
1247 return abuseRateLimitError
1248 default:
1249 return errorResponse
1250 }
1251 }
1252
1253
1254
1255
1256
1257
1258 func parseBoolResponse(err error) (bool, error) {
1259 if err == nil {
1260 return true, nil
1261 }
1262
1263 if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound {
1264
1265 return false, nil
1266 }
1267
1268
1269 return false, err
1270 }
1271
1272
1273 type Rate struct {
1274
1275 Limit int `json:"limit"`
1276
1277
1278 Remaining int `json:"remaining"`
1279
1280
1281 Reset Timestamp `json:"reset"`
1282 }
1283
1284 func (r Rate) String() string {
1285 return Stringify(r)
1286 }
1287
1288
1289 type RateLimits struct {
1290
1291
1292
1293
1294
1295 Core *Rate `json:"core"`
1296
1297
1298
1299
1300
1301
1302 Search *Rate `json:"search"`
1303
1304
1305 GraphQL *Rate `json:"graphql"`
1306
1307
1308 IntegrationManifest *Rate `json:"integration_manifest"`
1309
1310 SourceImport *Rate `json:"source_import"`
1311 CodeScanningUpload *Rate `json:"code_scanning_upload"`
1312 ActionsRunnerRegistration *Rate `json:"actions_runner_registration"`
1313 SCIM *Rate `json:"scim"`
1314 }
1315
1316 func (r RateLimits) String() string {
1317 return Stringify(r)
1318 }
1319
1320 type rateLimitCategory uint8
1321
1322 const (
1323 coreCategory rateLimitCategory = iota
1324 searchCategory
1325 graphqlCategory
1326 integrationManifestCategory
1327 sourceImportCategory
1328 codeScanningUploadCategory
1329 actionsRunnerRegistrationCategory
1330 scimCategory
1331
1332 categories
1333 )
1334
1335
1336 func category(method, path string) rateLimitCategory {
1337 switch {
1338
1339 default:
1340
1341
1342 return coreCategory
1343 case strings.HasPrefix(path, "/search/"):
1344 return searchCategory
1345 case path == "/graphql":
1346 return graphqlCategory
1347 case strings.HasPrefix(path, "/app-manifests/") &&
1348 strings.HasSuffix(path, "/conversions") &&
1349 method == http.MethodPost:
1350 return integrationManifestCategory
1351
1352
1353 case strings.HasPrefix(path, "/repos/") &&
1354 strings.HasSuffix(path, "/import") &&
1355 method == http.MethodPut:
1356 return sourceImportCategory
1357
1358
1359 case strings.HasSuffix(path, "/code-scanning/sarifs"):
1360 return codeScanningUploadCategory
1361
1362
1363 case strings.HasPrefix(path, "/scim/"):
1364 return scimCategory
1365 }
1366 }
1367
1368
1369 func (c *Client) RateLimits(ctx context.Context) (*RateLimits, *Response, error) {
1370 req, err := c.NewRequest("GET", "rate_limit", nil)
1371 if err != nil {
1372 return nil, nil, err
1373 }
1374
1375 response := new(struct {
1376 Resources *RateLimits `json:"resources"`
1377 })
1378
1379
1380 ctx = context.WithValue(ctx, bypassRateLimitCheck, true)
1381 resp, err := c.Do(ctx, req, response)
1382 if err != nil {
1383 return nil, resp, err
1384 }
1385
1386 if response.Resources != nil {
1387 c.rateMu.Lock()
1388 if response.Resources.Core != nil {
1389 c.rateLimits[coreCategory] = *response.Resources.Core
1390 }
1391 if response.Resources.Search != nil {
1392 c.rateLimits[searchCategory] = *response.Resources.Search
1393 }
1394 if response.Resources.GraphQL != nil {
1395 c.rateLimits[graphqlCategory] = *response.Resources.GraphQL
1396 }
1397 if response.Resources.IntegrationManifest != nil {
1398 c.rateLimits[integrationManifestCategory] = *response.Resources.IntegrationManifest
1399 }
1400 if response.Resources.SourceImport != nil {
1401 c.rateLimits[sourceImportCategory] = *response.Resources.SourceImport
1402 }
1403 if response.Resources.CodeScanningUpload != nil {
1404 c.rateLimits[codeScanningUploadCategory] = *response.Resources.CodeScanningUpload
1405 }
1406 if response.Resources.ActionsRunnerRegistration != nil {
1407 c.rateLimits[actionsRunnerRegistrationCategory] = *response.Resources.ActionsRunnerRegistration
1408 }
1409 if response.Resources.SCIM != nil {
1410 c.rateLimits[scimCategory] = *response.Resources.SCIM
1411 }
1412 c.rateMu.Unlock()
1413 }
1414
1415 return response.Resources, resp, nil
1416 }
1417
1418 func setCredentialsAsHeaders(req *http.Request, id, secret string) *http.Request {
1419
1420
1421
1422
1423
1424
1425 convertedRequest := new(http.Request)
1426 *convertedRequest = *req
1427 convertedRequest.Header = make(http.Header, len(req.Header))
1428
1429 for k, s := range req.Header {
1430 convertedRequest.Header[k] = append([]string(nil), s...)
1431 }
1432 convertedRequest.SetBasicAuth(id, secret)
1433 return convertedRequest
1434 }
1435
1436
1452 type UnauthenticatedRateLimitedTransport struct {
1453
1454
1455
1456 ClientID string
1457
1458
1459
1460 ClientSecret string
1461
1462
1463
1464 Transport http.RoundTripper
1465 }
1466
1467
1468 func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
1469 if t.ClientID == "" {
1470 return nil, errors.New("t.ClientID is empty")
1471 }
1472 if t.ClientSecret == "" {
1473 return nil, errors.New("t.ClientSecret is empty")
1474 }
1475
1476 req2 := setCredentialsAsHeaders(req, t.ClientID, t.ClientSecret)
1477
1478 return t.transport().RoundTrip(req2)
1479 }
1480
1481
1482
1483 func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client {
1484 return &http.Client{Transport: t}
1485 }
1486
1487 func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper {
1488 if t.Transport != nil {
1489 return t.Transport
1490 }
1491 return http.DefaultTransport
1492 }
1493
1494
1495
1496
1497
1498 type BasicAuthTransport struct {
1499 Username string
1500 Password string
1501 OTP string
1502
1503
1504
1505 Transport http.RoundTripper
1506 }
1507
1508
1509 func (t *BasicAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
1510 req2 := setCredentialsAsHeaders(req, t.Username, t.Password)
1511 if t.OTP != "" {
1512 req2.Header.Set(headerOTP, t.OTP)
1513 }
1514 return t.transport().RoundTrip(req2)
1515 }
1516
1517
1518
1519 func (t *BasicAuthTransport) Client() *http.Client {
1520 return &http.Client{Transport: t}
1521 }
1522
1523 func (t *BasicAuthTransport) transport() http.RoundTripper {
1524 if t.Transport != nil {
1525 return t.Transport
1526 }
1527 return http.DefaultTransport
1528 }
1529
1530
1531
1532
1533 func formatRateReset(d time.Duration) string {
1534 isNegative := d < 0
1535 if isNegative {
1536 d *= -1
1537 }
1538 secondsTotal := int(0.5 + d.Seconds())
1539 minutes := secondsTotal / 60
1540 seconds := secondsTotal - minutes*60
1541
1542 var timeString string
1543 if minutes > 0 {
1544 timeString = fmt.Sprintf("%dm%02ds", minutes, seconds)
1545 } else {
1546 timeString = fmt.Sprintf("%ds", seconds)
1547 }
1548
1549 if isNegative {
1550 return fmt.Sprintf("[rate limit was reset %v ago]", timeString)
1551 }
1552 return fmt.Sprintf("[rate reset in %v]", timeString)
1553 }
1554
1555
1556
1557 func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool, opts ...RequestOption) (*http.Response, error) {
1558 req, err := c.NewRequest("GET", u, nil, opts...)
1559 if err != nil {
1560 return nil, err
1561 }
1562
1563 var resp *http.Response
1564
1565 req = withContext(ctx, req)
1566 if c.client.Transport == nil {
1567 resp, err = http.DefaultTransport.RoundTrip(req)
1568 } else {
1569 resp, err = c.client.Transport.RoundTrip(req)
1570 }
1571 if err != nil {
1572 return nil, err
1573 }
1574
1575
1576 if followRedirects && resp.StatusCode == http.StatusMovedPermanently {
1577 resp.Body.Close()
1578 u = resp.Header.Get("Location")
1579 resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false, opts...)
1580 }
1581 return resp, err
1582 }
1583
1584
1585
1586 func Bool(v bool) *bool { return &v }
1587
1588
1589
1590 func Int(v int) *int { return &v }
1591
1592
1593
1594 func Int64(v int64) *int64 { return &v }
1595
1596
1597
1598 func String(v string) *string { return &v }
1599
1600
1601 type roundTripperFunc func(*http.Request) (*http.Response, error)
1602
1603 func (fn roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) {
1604 return fn(r)
1605 }
1606
View as plain text