diff --git a/github/github.go b/github/github.go index 38a5bcd29b..45235cf8f0 100644 --- a/github/github.go +++ b/github/github.go @@ -27,12 +27,14 @@ import ( ) const ( - Version = "v48.0.0" + Version = "v48.2.0" - defaultBaseURL = "https://api.github.com/" - defaultUserAgent = "go-github" + "/" + Version - uploadBaseURL = "https://uploads.github.com/" + defaultAPIVersion = "2022-11-28" + defaultBaseURL = "https://api.github.com/" + defaultUserAgent = "go-github" + "/" + Version + uploadBaseURL = "https://uploads.github.com/" + headerAPIVersion = "X-GitHub-Api-Version" headerRateLimit = "X-RateLimit-Limit" headerRateRemaining = "X-RateLimit-Remaining" headerRateReset = "X-RateLimit-Reset" @@ -392,12 +394,24 @@ func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*C return c, nil } +// RequestOption represents an option that can modify an http.Request. +type RequestOption func(req *http.Request) + +// WithVersion modifies the GitHub v3 API version for this individual request. +// For more information, see: +// https://github.blog/2022-11-28-to-infinity-and-beyond-enabling-the-future-of-githubs-rest-api-with-api-versioning/ +func WithVersion(version string) RequestOption { + return func(req *http.Request) { + req.Header.Set(headerAPIVersion, version) + } +} + // NewRequest creates an API request. A relative URL can be provided in urlStr, // in which case it is resolved relative to the BaseURL of the Client. // Relative URLs should always be specified without a preceding slash. If // specified, the value pointed to by body is JSON encoded and included as the // request body. -func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { +func (c *Client) NewRequest(method, urlStr string, body interface{}, opts ...RequestOption) (*http.Request, error) { if !strings.HasSuffix(c.BaseURL.Path, "/") { return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) } @@ -430,6 +444,12 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ if c.UserAgent != "" { req.Header.Set("User-Agent", c.UserAgent) } + req.Header.Set(headerAPIVersion, defaultAPIVersion) + + for _, opt := range opts { + opt(req) + } + return req, nil } @@ -437,7 +457,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ // in which case it is resolved relative to the BaseURL of the Client. // Relative URLs should always be specified without a preceding slash. // Body is sent with Content-Type: application/x-www-form-urlencoded. -func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, error) { +func (c *Client) NewFormRequest(urlStr string, body io.Reader, opts ...RequestOption) (*http.Request, error) { if !strings.HasSuffix(c.BaseURL.Path, "/") { return nil, fmt.Errorf("BaseURL must have a trailing slash, but %q does not", c.BaseURL) } @@ -457,13 +477,19 @@ func (c *Client) NewFormRequest(urlStr string, body io.Reader) (*http.Request, e if c.UserAgent != "" { req.Header.Set("User-Agent", c.UserAgent) } + req.Header.Set(headerAPIVersion, defaultAPIVersion) + + for _, opt := range opts { + opt(req) + } + return req, nil } // NewUploadRequest creates an upload request. A relative URL can be provided in // urlStr, in which case it is resolved relative to the UploadURL of the Client. // Relative URLs should always be specified without a preceding slash. -func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string) (*http.Request, error) { +func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, mediaType string, opts ...RequestOption) (*http.Request, error) { if !strings.HasSuffix(c.UploadURL.Path, "/") { return nil, fmt.Errorf("UploadURL must have a trailing slash, but %q does not", c.UploadURL) } @@ -485,6 +511,12 @@ func (c *Client) NewUploadRequest(urlStr string, reader io.Reader, size int64, m req.Header.Set("Content-Type", mediaType) req.Header.Set("Accept", mediaTypeV3) req.Header.Set("User-Agent", c.UserAgent) + req.Header.Set(headerAPIVersion, defaultAPIVersion) + + for _, opt := range opts { + opt(req) + } + return req, nil } @@ -1358,8 +1390,8 @@ func formatRateReset(d time.Duration) string { // When using roundTripWithOptionalFollowRedirect, note that it // is the responsibility of the caller to close the response body. -func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool) (*http.Response, error) { - req, err := c.NewRequest("GET", u, nil) +func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u string, followRedirects bool, opts ...RequestOption) (*http.Response, error) { + req, err := c.NewRequest("GET", u, nil, opts...) if err != nil { return nil, err } @@ -1380,7 +1412,7 @@ func (c *Client) roundTripWithOptionalFollowRedirect(ctx context.Context, u stri if followRedirects && resp.StatusCode == http.StatusMovedPermanently { resp.Body.Close() u = resp.Header.Get("Location") - resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false) + resp, err = c.roundTripWithOptionalFollowRedirect(ctx, u, false, opts...) } return resp, err } diff --git a/github/github_test.go b/github/github_test.go index c4ceb89bb5..42e3ce2ce9 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -517,6 +517,17 @@ func TestNewRequest(t *testing.T) { if !strings.Contains(userAgent, Version) { t.Errorf("NewRequest() User-Agent should contain %v, found %v", Version, userAgent) } + + apiVersion := req.Header.Get(headerAPIVersion) + if got, want := apiVersion, defaultAPIVersion; got != want { + t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want) + } + + req, _ = c.NewRequest("GET", inURL, inBody, WithVersion("2022-11-29")) + apiVersion = req.Header.Get(headerAPIVersion) + if got, want := apiVersion, "2022-11-29"; got != want { + t.Errorf("NewRequest() %v header is %v, want %v", headerAPIVersion, got, want) + } } func TestNewRequest_invalidJSON(t *testing.T) {