Repository: google/go-github
Pull request #1960 (2021-07-09 21:58:22)

Add ListOptions to GetCommit and CompareCommits to support pagination

Saaarah: > Thanks, @Saaarah.
> 
> Please make sure to run unit tests before submitting PRs.
> You will need to modify the unit tests since the API has changed with this PR.
> 
> Please check out surrounding tests to see how pagination is also tested. Thanks.

Hi Glenn, thanks for reviewing my merge request and I've added the new parameters in the two API's unit tests. Thanks again!
gmlewis: Hi @Saaarah - it looks like there are some calls to these APIs in the examples, if you could please take a look and fix them.

Here is the error:

```
Run go vet ./...
# github.com/google/go-github/v37/example/commitpr
vet: example/commitpr/main.go:128:97: too few arguments in call to client.Repositories.GetCommit
Error: Process completed with exit code 2.
```
Saaarah: > Hi @Saaarah - it looks like there are some calls to these APIs in the examples, if you could please take a look and fix them.
> 
> Here is the error:
> 
> ```
> Run go vet ./...
> # github.com/google/go-github/v37/example/commitpr
> vet: example/commitpr/main.go:128:97: too few arguments in call to client.Repositories.GetCommit
> Error: Process completed with exit code 2.
> ```

Hi Glenn, I've fixed the example. Sorry for not updating all affected code, it'll be nice to have a small section in readme.md to ask contributors to run all necessary tests prior to creating pull request, which will be really helpful for first timer contributors.
gmlewis: > it'll be nice to have a small section in readme.md to ask contributors to run all necessary tests prior to creating pull request, which will be really helpful for first timer contributors.

This is already covered in our [CONTRIBUTING.md](https://github.com/google/go-github/blob/master/CONTRIBUTING.md) doc:

```
  5. Please run:
     * `go generate github.com/google/go-github/...`
     * `go test github.com/google/go-github/...`
     * `go vet github.com/google/go-github/...`
```
gmlewis: It now looks like `gofmt` needs to be run on the example code:

```
Run diff -u <(echo -n) <(gofmt -d -s .)
--- /dev/fd/63	2021-07-12 15:58:15.735418376 +0000
+++ /dev/fd/62	2021-07-12 15:58:15.735418376 +0000
@@ -0,0 +1,12 @@
+diff -u example/commitpr/main.go.orig example/commitpr/main.go
+--- example/commitpr/main.go.orig	2021-07-12 15:58:16.035434090 +0000
++++ example/commitpr/main.go	2021-07-12 15:58:16.035434090 +0000
+@@ -127,7 +127,7 @@
+ 	// Get the parent commit to attach the commit to.
+ 	opt := &github.ListOptions{
+ 		PerPage: 10,
+-		Page: 1,
++		Page:    1,
+ 	}
+ 	parent, _, err := client.Repositories.GetCommit(ctx, *sourceOwner, *sourceRepo, *ref.Object.SHA, opt)
+ 	if err != nil {
Error: Process completed with exit code 1.
```
Saaarah: > > it'll be nice to have a small section in readme.md to ask contributors to run all necessary tests prior to creating pull request, which will be really helpful for first timer contributors.
> 
> This is already covered in our [CONTRIBUTING.md](https://github.com/google/go-github/blob/master/CONTRIBUTING.md) doc:
> 
> ```
>   5. Please run:
>      * `go generate github.com/google/go-github/...`
>      * `go test github.com/google/go-github/...`
>      * `go vet github.com/google/go-github/...`
> ```

Ah thanks for pointing out! I've formatted the file and run all the commands listed above. Thanks Glenn!
codecov[bot]: # [Codecov](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> Merging [#1960](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (89c3cfc) into [master](https://codecov.io/gh/google/go-github/commit/f47f8fe25bcaf870974ff18ae62022c32c164966?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (f47f8fe) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/google/go-github/pull/1960/graphs/tree.svg?width=650&height=150&src=pr&token=EgYjGjZlI0&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)

```diff
@@           Coverage Diff           @@
##           master    #1960   +/-   ##
=======================================
  Coverage   97.84%   97.84%           
=======================================
  Files         105      105           
  Lines        6809     6815    +6     
=======================================
+ Hits         6662     6668    +6     
  Misses         80       80           
  Partials       67       67           
```


| [Impacted Files](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | Coverage Δ | |
|---|---|---|
| [github/repos\_commits.go](https://codecov.io/gh/google/go-github/pull/1960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL3JlcG9zX2NvbW1pdHMuZ28=) | `100.00% <100.00%> (ø)` | |
| [github/checks.go](https://codecov.io/gh/google/go-github/pull/1960/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2NoZWNrcy5nbw==) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Last update [f47f8fe...89c3cfc](https://codecov.io/gh/google/go-github/pull/1960?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).


Patch:
diff --git a/example/commitpr/main.go b/example/commitpr/main.go
index 33572f20..d222b88f 100644
--- a/example/commitpr/main.go
+++ b/example/commitpr/main.go
@@ -125,7 +125,7 @@ func getFileContent(fileArg string) (targetName string, b []byte, err error) {
 // pushCommit creates the commit in the given reference using the given tree.
 func pushCommit(ref *github.Reference, tree *github.Tree) (err error) {
 	// Get the parent commit to attach the commit to.
-	parent, _, err := client.Repositories.GetCommit(ctx, *sourceOwner, *sourceRepo, *ref.Object.SHA)
+	parent, _, err := client.Repositories.GetCommit(ctx, *sourceOwner, *sourceRepo, *ref.Object.SHA, nil)
 	if err != nil {
 		return err
 	}
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 314b129d..fb3e2903 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -4852,6 +4852,142 @@ func (h *HookConfig) GetURL() string {
 	return *h.URL
 }
 
+// GetAction returns the Action field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetAction() string {
+	if h == nil || h.Action == nil {
+		return ""
+	}
+	return *h.Action
+}
+
+// GetDeliveredAt returns the DeliveredAt field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetDeliveredAt() Timestamp {
+	if h == nil || h.DeliveredAt == nil {
+		return Timestamp{}
+	}
+	return *h.DeliveredAt
+}
+
+// GetDuration returns the Duration field.
+func (h *HookDelivery) GetDuration() *float64 {
+	if h == nil {
+		return nil
+	}
+	return h.Duration
+}
+
+// GetEvent returns the Event field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetEvent() string {
+	if h == nil || h.Event == nil {
+		return ""
+	}
+	return *h.Event
+}
+
+// GetGUID returns the GUID field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetGUID() string {
+	if h == nil || h.GUID == nil {
+		return ""
+	}
+	return *h.GUID
+}
+
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetID() int64 {
+	if h == nil || h.ID == nil {
+		return 0
+	}
+	return *h.ID
+}
+
+// GetInstallationID returns the InstallationID field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetInstallationID() string {
+	if h == nil || h.InstallationID == nil {
+		return ""
+	}
+	return *h.InstallationID
+}
+
+// GetRedelivery returns the Redelivery field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetRedelivery() bool {
+	if h == nil || h.Redelivery == nil {
+		return false
+	}
+	return *h.Redelivery
+}
+
+// GetRepositoryID returns the RepositoryID field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetRepositoryID() int64 {
+	if h == nil || h.RepositoryID == nil {
+		return 0
+	}
+	return *h.RepositoryID
+}
+
+// GetRequest returns the Request field.
+func (h *HookDelivery) GetRequest() *HookRequest {
+	if h == nil {
+		return nil
+	}
+	return h.Request
+}
+
+// GetResponse returns the Response field.
+func (h *HookDelivery) GetResponse() *HookResponse {
+	if h == nil {
+		return nil
+	}
+	return h.Response
+}
+
+// GetStatus returns the Status field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetStatus() string {
+	if h == nil || h.Status == nil {
+		return ""
+	}
+	return *h.Status
+}
+
+// GetStatusCode returns the StatusCode field if it's non-nil, zero value otherwise.
+func (h *HookDelivery) GetStatusCode() int {
+	if h == nil || h.StatusCode == nil {
+		return 0
+	}
+	return *h.StatusCode
+}
+
+// GetHeaders returns the Headers map if it's non-nil, an empty map otherwise.
+func (h *HookRequest) GetHeaders() map[string]string {
+	if h == nil || h.Headers == nil {
+		return map[string]string{}
+	}
+	return h.Headers
+}
+
+// GetRawPayload returns the RawPayload field if it's non-nil, zero value otherwise.
+func (h *HookRequest) GetRawPayload() json.RawMessage {
+	if h == nil || h.RawPayload == nil {
+		return json.RawMessage{}
+	}
+	return *h.RawPayload
+}
+
+// GetHeaders returns the Headers map if it's non-nil, an empty map otherwise.
+func (h *HookResponse) GetHeaders() map[string]string {
+	if h == nil || h.Headers == nil {
+		return map[string]string{}
+	}
+	return h.Headers
+}
+
+// GetRawPayload returns the RawPayload field if it's non-nil, zero value otherwise.
+func (h *HookResponse) GetRawPayload() json.RawMessage {
+	if h == nil || h.RawPayload == nil {
+		return json.RawMessage{}
+	}
+	return *h.RawPayload
+}
+
 // GetActiveHooks returns the ActiveHooks field if it's non-nil, zero value otherwise.
 func (h *HookStats) GetActiveHooks() int {
 	if h == nil || h.ActiveHooks == nil {
diff --git a/github/github.go b/github/github.go
index 264e27f4..ae0ee08c 100644
--- a/github/github.go
+++ b/github/github.go
@@ -212,6 +212,9 @@ type ListCursorOptions struct {
 
 	// A cursor, as given in the Link header. If specified, the query only searches for events before this cursor.
 	Before string `url:"before,omitempty"`
+
+	// A cursor, as given in the Link header. If specified, the query continues the search using this cursor.
+	Cursor string `url:"cursor,omitempty"`
 }
 
 // UploadOptions specifies the parameters to methods that support uploads.
@@ -445,6 +448,11 @@ type Response struct {
 	// calling the endpoint again.
 	NextPageToken string
 
+	// For APIs that support cursor pagination, such as RepositoryService.ListRepositoryHookDeliveries,
+	// the following field will be populated to point to the next page.
+	// Set ListCursorOptions.Cursor to this value when calling the endpoint again.
+	Cursor string
+
 	// Explicitly specify the Rate type so Rate's String() receiver doesn't
 	// propagate to Response.
 	Rate Rate
@@ -481,7 +489,21 @@ func (r *Response) populatePageValues() {
 			if err != nil {
 				continue
 			}
-			page := url.Query().Get("page")
+
+			q := url.Query()
+
+			if cursor := q.Get("cursor"); cursor != "" {
+				for _, segment := range segments[1:] {
+					switch strings.TrimSpace(segment) {
+					case `rel="next"`:
+						r.Cursor = cursor
+					}
+				}
+
+				continue
+			}
+
+			page := q.Get("page")
 			if page == "" {
 				continue
 			}
@@ -499,7 +521,6 @@ func (r *Response) populatePageValues() {
 				case `rel="last"`:
 					r.LastPage, _ = strconv.Atoi(page)
 				}
-
 			}
 		}
 	}
diff --git a/github/messages.go b/github/messages.go
index 06ea9e4c..caa596a2 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -18,6 +18,7 @@ import (
 	"errors"
 	"fmt"
 	"hash"
+	"io"
 	"io/ioutil"
 	"mime"
 	"net/http"
@@ -31,14 +32,14 @@ const (
 	// sha256Prefix and sha512Prefix are provided for future compatibility.
 	sha256Prefix = "sha256"
 	sha512Prefix = "sha512"
-	// sha1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest.
-	sha1SignatureHeader = "X-Hub-Signature"
-	// sha256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest.
-	sha256SignatureHeader = "X-Hub-Signature-256"
-	// eventTypeHeader is the GitHub header key used to pass the event type.
-	eventTypeHeader = "X-Github-Event"
-	// deliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event.
-	deliveryIDHeader = "X-Github-Delivery"
+	// SHA1SignatureHeader is the GitHub header key used to pass the HMAC-SHA1 hexdigest.
+	SHA1SignatureHeader = "X-Hub-Signature"
+	// SHA256SignatureHeader is the GitHub header key used to pass the HMAC-SHA256 hexdigest.
+	SHA256SignatureHeader = "X-Hub-Signature-256"
+	// EventTypeHeader is the GitHub header key used to pass the event type.
+	EventTypeHeader = "X-Github-Event"
+	// DeliveryIDHeader is the GitHub header key used to pass the unique ID for the webhook event.
+	DeliveryIDHeader = "X-Github-Delivery"
 )
 
 var (
@@ -139,7 +140,7 @@ func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
 	return buf, hashFunc, nil
 }
 
-// ValidatePayload validates an incoming GitHub Webhook event request
+// ValidatePayload validates an incoming GitHub Webhook event request body
 // and returns the (JSON) payload.
 // The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
 // If the Content-Type is neither then an error is returned.
@@ -150,25 +151,19 @@ func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
 // Example usage:
 //
 //     func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-//       payload, err := github.ValidatePayload(r, s.webhookSecretKey)
+//       // read signature from request
+//       signature := ""
+//       payload, err := github.ValidatePayloadFromBody(r.Header.Get("Content-Type"), r.Body, signature, s.webhookSecretKey)
 //       if err != nil { ... }
 //       // Process payload...
 //     }
-//
-func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
+func ValidatePayloadFromBody(contentType string, readable io.Reader, signature string, secretToken []byte) (payload []byte, err error) {
 	var body []byte // Raw body that GitHub uses to calculate the signature.
 
-	ct := r.Header.Get("Content-Type")
-
-	mediatype, _, err := mime.ParseMediaType(ct)
-	if err != nil {
-		mediatype = ""
-	}
-
-	switch mediatype {
+	switch contentType {
 	case "application/json":
 		var err error
-		if body, err = ioutil.ReadAll(r.Body); err != nil {
+		if body, err = ioutil.ReadAll(readable); err != nil {
 			return nil, err
 		}
 
@@ -182,7 +177,7 @@ func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err e
 		const payloadFormParam = "payload"
 
 		var err error
-		if body, err = ioutil.ReadAll(r.Body); err != nil {
+		if body, err = ioutil.ReadAll(readable); err != nil {
 			return nil, err
 		}
 
@@ -195,17 +190,13 @@ func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err e
 		payload = []byte(form.Get(payloadFormParam))
 
 	default:
-		return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct)
+		return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", contentType)
 	}
 
 	// Only validate the signature if a secret token exists. This is intended for
 	// local development only and all webhooks should ideally set up a secret token.
 	if len(secretToken) > 0 {
-		sig := r.Header.Get(sha256SignatureHeader)
-		if sig == "" {
-			sig = r.Header.Get(sha1SignatureHeader)
-		}
-		if err := ValidateSignature(sig, body, secretToken); err != nil {
+		if err := ValidateSignature(signature, body, secretToken); err != nil {
 			return nil, err
 		}
 	}
@@ -213,6 +204,36 @@ func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err e
 	return payload, nil
 }
 
+// ValidatePayload validates an incoming GitHub Webhook event request
+// and returns the (JSON) payload.
+// The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
+// If the Content-Type is neither then an error is returned.
+// secretToken is the GitHub Webhook secret token.
+// If your webhook does not contain a secret token, you can pass nil or an empty slice.
+// This is intended for local development purposes only and all webhooks should ideally set up a secret token.
+//
+// Example usage:
+//
+//     func (s *GitHubEventMonitor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+//       payload, err := github.ValidatePayload(r, s.webhookSecretKey)
+//       if err != nil { ... }
+//       // Process payload...
+//     }
+//
+func ValidatePayload(r *http.Request, secretToken []byte) (payload []byte, err error) {
+	signature := r.Header.Get(SHA256SignatureHeader)
+	if signature == "" {
+		signature = r.Header.Get(SHA1SignatureHeader)
+	}
+
+	contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
+	if err != nil {
+		return nil, err
+	}
+
+	return ValidatePayloadFromBody(contentType, r.Body, signature, secretToken)
+}
+
 // ValidateSignature validates the signature for the given payload.
 // signature is the GitHub hash signature delivered in the X-Hub-Signature header.
 // payload is the JSON payload sent by GitHub Webhooks.
@@ -234,14 +255,14 @@ func ValidateSignature(signature string, payload, secretToken []byte) error {
 //
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers
 func WebHookType(r *http.Request) string {
-	return r.Header.Get(eventTypeHeader)
+	return r.Header.Get(EventTypeHeader)
 }
 
 // DeliveryID returns the unique delivery ID of webhook request r.
 //
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers
 func DeliveryID(r *http.Request) string {
-	return r.Header.Get(deliveryIDHeader)
+	return r.Header.Get(DeliveryIDHeader)
 }
 
 // ParseWebHook parses the event payload. For recognized event types, a
diff --git a/github/orgs_hooks_deliveries.go b/github/orgs_hooks_deliveries.go
new file mode 100644
index 00000000..6ab2d7aa
--- /dev/null
+++ b/github/orgs_hooks_deliveries.go
@@ -0,0 +1,54 @@
+// Copyright 2021 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"fmt"
+)
+
+// ListHookDeliveries lists webhook deliveries for a webhook configured in an organization.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/orgs#list-deliveries-for-an-organization-webhook
+func (s *OrganizationsService) ListHookDeliveries(ctx context.Context, org string, id int64, opts *ListCursorOptions) ([]*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/hooks/%v/deliveries", org, id)
+	u, err := addOptions(u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	deliveries := []*HookDelivery{}
+	resp, err := s.client.Do(ctx, req, &deliveries)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return deliveries, resp, nil
+}
+
+// GetHookDelivery returns a delivery for a webhook configured in an organization.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/orgs#get-a-webhook-delivery-for-an-organization-webhook
+func (s *OrganizationsService) GetHookDelivery(ctx context.Context, owner string, hookID, deliveryID int64) (*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/hooks/%v/deliveries/%v", owner, hookID, deliveryID)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	h := new(HookDelivery)
+	resp, err := s.client.Do(ctx, req, h)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return h, resp, nil
+}
diff --git a/github/repos_commits.go b/github/repos_commits.go
index b49a5afe..ce3b48e3 100644
--- a/github/repos_commits.go
+++ b/github/repos_commits.go
@@ -150,8 +150,12 @@ func (s *RepositoriesService) ListCommits(ctx context.Context, owner, repo strin
 //
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#get-a-single-commit
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#get-a-commit
-func (s *RepositoriesService) GetCommit(ctx context.Context, owner, repo, sha string) (*RepositoryCommit, *Response, error) {
+func (s *RepositoriesService) GetCommit(ctx context.Context, owner, repo, sha string, opts *ListOptions) (*RepositoryCommit, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/commits/%v", owner, repo, sha)
+	u, err := addOptions(u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
 
 	req, err := s.client.NewRequest("GET", u, nil)
 	if err != nil {
@@ -224,11 +228,15 @@ func (s *RepositoriesService) GetCommitSHA1(ctx context.Context, owner, repo, re
 // CompareCommits compares a range of commits with each other.
 //
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#compare-two-commits
-func (s *RepositoriesService) CompareCommits(ctx context.Context, owner, repo string, base, head string) (*CommitsComparison, *Response, error) {
+func (s *RepositoriesService) CompareCommits(ctx context.Context, owner, repo string, base, head string, opts *ListOptions) (*CommitsComparison, *Response, error) {
 	escapedBase := url.QueryEscape(base)
 	escapedHead := url.QueryEscape(head)
 
 	u := fmt.Sprintf("repos/%v/%v/compare/%v...%v", owner, repo, escapedBase, escapedHead)
+	u, err := addOptions(u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
 
 	req, err := s.client.NewRequest("GET", u, nil)
 	if err != nil {
diff --git a/github/repos_hooks_deliveries.go b/github/repos_hooks_deliveries.go
new file mode 100644
index 00000000..12267446
--- /dev/null
+++ b/github/repos_hooks_deliveries.go
@@ -0,0 +1,117 @@
+// Copyright 2021 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+)
+
+// HookDelivery represents the data that is received from GitHub's Webhook Delivery API
+//
+// GitHub API docs:
+// - https://docs.github.com/en/rest/reference/repos#list-deliveries-for-a-repository-webhook
+// - https://docs.github.com/en/rest/reference/repos#get-a-delivery-for-a-repository-webhook
+type HookDelivery struct {
+	ID             *int64     `json:"id"`
+	GUID           *string    `json:"guid"`
+	DeliveredAt    *Timestamp `json:"delivered_at"`
+	Redelivery     *bool      `json:"redelivery"`
+	Duration       *float64   `json:"duration"`
+	Status         *string    `json:"status"`
+	StatusCode     *int       `json:"status_code"`
+	Event          *string    `json:"event"`
+	Action         *string    `json:"action"`
+	InstallationID *string    `json:"installation_id"`
+	RepositoryID   *int64     `json:"repository_id"`
+
+	// Request is populated by GetHookDelivery.
+	Request *HookRequest `json:"request,omitempty"`
+	// Response is populated by GetHookDelivery.
+	Response *HookResponse `json:"response,omitempty"`
+}
+
+func (d HookDelivery) String() string {
+	return Stringify(d)
+}
+
+// HookRequest is a part of HookDelivery that contains
+// the HTTP headers and the JSON payload of the webhook request.
+type HookRequest struct {
+	Headers    map[string]string `json:"headers,omitempty"`
+	RawPayload *json.RawMessage  `json:"payload,omitempty"`
+}
+
+func (r HookRequest) String() string {
+	return Stringify(r)
+}
+
+// HookResponse is a part of HookDelivery that contains
+// the HTTP headers and the response body served by the webhook endpoint.
+type HookResponse struct {
+	Headers    map[string]string `json:"headers,omitempty"`
+	RawPayload *json.RawMessage  `json:"payload,omitempty"`
+}
+
+func (r HookResponse) String() string {
+	return Stringify(r)
+}
+
+// ListHookDeliveries lists webhook deliveries for a webhook configured in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/repos#list-deliveries-for-a-repository-webhook
+func (s *RepositoriesService) ListHookDeliveries(ctx context.Context, owner, repo string, id int64, opts *ListCursorOptions) ([]*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/hooks/%v/deliveries", owner, repo, id)
+	u, err := addOptions(u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	deliveries := []*HookDelivery{}
+	resp, err := s.client.Do(ctx, req, &deliveries)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return deliveries, resp, nil
+}
+
+// GetHookDelivery returns a delivery for a webhook configured in a repository.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/repos#get-a-delivery-for-a-repository-webhook
+func (s *RepositoriesService) GetHookDelivery(ctx context.Context, owner, repo string, hookID, deliveryID int64) (*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/hooks/%v/deliveries/%v", owner, repo, hookID, deliveryID)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	h := new(HookDelivery)
+	resp, err := s.client.Do(ctx, req, h)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return h, resp, nil
+}
+
+// ParseRequestPayload parses the request payload. For recognized event types,
+// a value of the corresponding struct type will be returned.
+func (d *HookDelivery) ParseRequestPayload() (interface{}, error) {
+	eType, ok := eventTypeMapping[*d.Event]
+	if !ok {
+		return nil, fmt.Errorf("unsupported event type %q", *d.Event)
+	}
+
+	e := &Event{Type: &eType, RawPayload: d.Request.RawPayload}
+	return e.ParsePayload()
+}


Test patch:
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index 914c2c04..03fd08ea 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -5682,6 +5682,167 @@ func TestHookConfig_GetURL(tt *testing.T) {
 	h.GetURL()
 }
 
+func TestHookDelivery_GetAction(tt *testing.T) {
+	var zeroValue string
+	h := &HookDelivery{Action: &zeroValue}
+	h.GetAction()
+	h = &HookDelivery{}
+	h.GetAction()
+	h = nil
+	h.GetAction()
+}
+
+func TestHookDelivery_GetDeliveredAt(tt *testing.T) {
+	var zeroValue Timestamp
+	h := &HookDelivery{DeliveredAt: &zeroValue}
+	h.GetDeliveredAt()
+	h = &HookDelivery{}
+	h.GetDeliveredAt()
+	h = nil
+	h.GetDeliveredAt()
+}
+
+func TestHookDelivery_GetDuration(tt *testing.T) {
+	h := &HookDelivery{}
+	h.GetDuration()
+	h = nil
+	h.GetDuration()
+}
+
+func TestHookDelivery_GetEvent(tt *testing.T) {
+	var zeroValue string
+	h := &HookDelivery{Event: &zeroValue}
+	h.GetEvent()
+	h = &HookDelivery{}
+	h.GetEvent()
+	h = nil
+	h.GetEvent()
+}
+
+func TestHookDelivery_GetGUID(tt *testing.T) {
+	var zeroValue string
+	h := &HookDelivery{GUID: &zeroValue}
+	h.GetGUID()
+	h = &HookDelivery{}
+	h.GetGUID()
+	h = nil
+	h.GetGUID()
+}
+
+func TestHookDelivery_GetID(tt *testing.T) {
+	var zeroValue int64
+	h := &HookDelivery{ID: &zeroValue}
+	h.GetID()
+	h = &HookDelivery{}
+	h.GetID()
+	h = nil
+	h.GetID()
+}
+
+func TestHookDelivery_GetInstallationID(tt *testing.T) {
+	var zeroValue string
+	h := &HookDelivery{InstallationID: &zeroValue}
+	h.GetInstallationID()
+	h = &HookDelivery{}
+	h.GetInstallationID()
+	h = nil
+	h.GetInstallationID()
+}
+
+func TestHookDelivery_GetRedelivery(tt *testing.T) {
+	var zeroValue bool
+	h := &HookDelivery{Redelivery: &zeroValue}
+	h.GetRedelivery()
+	h = &HookDelivery{}
+	h.GetRedelivery()
+	h = nil
+	h.GetRedelivery()
+}
+
+func TestHookDelivery_GetRepositoryID(tt *testing.T) {
+	var zeroValue int64
+	h := &HookDelivery{RepositoryID: &zeroValue}
+	h.GetRepositoryID()
+	h = &HookDelivery{}
+	h.GetRepositoryID()
+	h = nil
+	h.GetRepositoryID()
+}
+
+func TestHookDelivery_GetRequest(tt *testing.T) {
+	h := &HookDelivery{}
+	h.GetRequest()
+	h = nil
+	h.GetRequest()
+}
+
+func TestHookDelivery_GetResponse(tt *testing.T) {
+	h := &HookDelivery{}
+	h.GetResponse()
+	h = nil
+	h.GetResponse()
+}
+
+func TestHookDelivery_GetStatus(tt *testing.T) {
+	var zeroValue string
+	h := &HookDelivery{Status: &zeroValue}
+	h.GetStatus()
+	h = &HookDelivery{}
+	h.GetStatus()
+	h = nil
+	h.GetStatus()
+}
+
+func TestHookDelivery_GetStatusCode(tt *testing.T) {
+	var zeroValue int
+	h := &HookDelivery{StatusCode: &zeroValue}
+	h.GetStatusCode()
+	h = &HookDelivery{}
+	h.GetStatusCode()
+	h = nil
+	h.GetStatusCode()
+}
+
+func TestHookRequest_GetHeaders(tt *testing.T) {
+	zeroValue := map[string]string{}
+	h := &HookRequest{Headers: zeroValue}
+	h.GetHeaders()
+	h = &HookRequest{}
+	h.GetHeaders()
+	h = nil
+	h.GetHeaders()
+}
+
+func TestHookRequest_GetRawPayload(tt *testing.T) {
+	var zeroValue json.RawMessage
+	h := &HookRequest{RawPayload: &zeroValue}
+	h.GetRawPayload()
+	h = &HookRequest{}
+	h.GetRawPayload()
+	h = nil
+	h.GetRawPayload()
+}
+
+func TestHookResponse_GetHeaders(tt *testing.T) {
+	zeroValue := map[string]string{}
+	h := &HookResponse{Headers: zeroValue}
+	h.GetHeaders()
+	h = &HookResponse{}
+	h.GetHeaders()
+	h = nil
+	h.GetHeaders()
+}
+
+func TestHookResponse_GetRawPayload(tt *testing.T) {
+	var zeroValue json.RawMessage
+	h := &HookResponse{RawPayload: &zeroValue}
+	h.GetRawPayload()
+	h = &HookResponse{}
+	h.GetRawPayload()
+	h = nil
+	h.GetRawPayload()
+}
+
 func TestHookStats_GetActiveHooks(tt *testing.T) {
 	var zeroValue int
 	h := &HookStats{ActiveHooks: &zeroValue}
diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go
index b86a4e73..0a852a60 100644
--- a/github/github-stringify_test.go
+++ b/github/github-stringify_test.go
@@ -546,6 +546,28 @@ func TestHook_String(t *testing.T) {
 	}
 }
 
+func TestHookDelivery_String(t *testing.T) {
+	v := HookDelivery{
+		ID:             Int64(0),
+		GUID:           String(""),
+		DeliveredAt:    &Timestamp{},
+		Redelivery:     Bool(false),
+		Duration:       Float64(0.0),
+		Status:         String(""),
+		StatusCode:     Int(0),
+		Event:          String(""),
+		Action:         String(""),
+		InstallationID: String(""),
+		RepositoryID:   Int64(0),
+		Request:        &HookRequest{},
+		Response:       &HookResponse{},
+	}
+	want := `github.HookDelivery{ID:0, GUID:"", DeliveredAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Redelivery:false, Duration:0, Status:"", StatusCode:0, Event:"", Action:"", InstallationID:"", RepositoryID:0, Request:github.HookRequest{}, Response:github.HookResponse{}}`
+	if got := v.String(); got != want {
+		t.Errorf("HookDelivery.String = %v, want %v", got, want)
+	}
+}
+
 func TestHookStats_String(t *testing.T) {
 	v := HookStats{
 		TotalHooks:    Int(0),
diff --git a/github/github_test.go b/github/github_test.go
index 158510ab..e7c995cc 100644
--- a/github/github_test.go
+++ b/github/github_test.go
@@ -638,6 +638,20 @@ func TestResponse_cursorPagination(t *testing.T) {
 	if got, want := response.NextPageToken, "url-encoded-next-page-token"; want != got {
 		t.Errorf("response.NextPageToken: %v, want %v", got, want)
 	}
+
+	// cursor-based pagination with "cursor" param
+	r = http.Response{
+		Header: http.Header{
+			"Link": {
+				`<https://api.github.com/?cursor=v1_12345678>; rel="next"`,
+			},
+		},
+	}
+
+	response = newResponse(&r)
+	if got, want := response.Cursor, "v1_12345678"; got != want {
+		t.Errorf("response.Cursor: %v, want %v", got, want)
+	}
 }
 
 func TestResponse_populatePageValues_invalid(t *testing.T) {
diff --git a/github/messages_test.go b/github/messages_test.go
index 5fed4a2d..9880a2a5 100644
--- a/github/messages_test.go
+++ b/github/messages_test.go
@@ -66,7 +66,7 @@ func TestValidatePayload(t *testing.T) {
 		},
 		{
 			signature:       "sha256=b1f8020f5b4cd42042f807dd939015c4a418bc1ff7f604dd55b0a19b5d953d9b",
-			signatureHeader: sha256SignatureHeader,
+			signatureHeader: SHA256SignatureHeader,
 			event:           "ping",
 			wantEvent:       "ping",
 			wantPayload:     defaultBody,
@@ -89,7 +89,7 @@ func TestValidatePayload(t *testing.T) {
 			if test.signatureHeader != "" {
 				req.Header.Set(test.signatureHeader, test.signature)
 			} else {
-				req.Header.Set(sha1SignatureHeader, test.signature)
+				req.Header.Set(SHA1SignatureHeader, test.signature)
 			}
 		}
 		req.Header.Set("Content-Type", "application/json")
@@ -120,7 +120,7 @@ func TestValidatePayload_FormGet(t *testing.T) {
 	}
 	req.PostForm = form
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-	req.Header.Set(sha1SignatureHeader, signature)
+	req.Header.Set(SHA1SignatureHeader, signature)
 
 	got, err := ValidatePayload(req, secretKey)
 	if err != nil {
@@ -131,7 +131,7 @@ func TestValidatePayload_FormGet(t *testing.T) {
 	}
 
 	// check that if payload is invalid we get error
-	req.Header.Set(sha1SignatureHeader, "invalid signature")
+	req.Header.Set(SHA1SignatureHeader, "invalid signature")
 	if _, err = ValidatePayload(req, []byte{0}); err == nil {
 		t.Error("ValidatePayload = nil, want err")
 	}
@@ -150,7 +150,7 @@ func TestValidatePayload_FormPost(t *testing.T) {
 		t.Fatalf("NewRequest: %v", err)
 	}
 	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
-	req.Header.Set(sha1SignatureHeader, signature)
+	req.Header.Set(SHA1SignatureHeader, signature)
 
 	got, err := ValidatePayload(req, secretKey)
 	if err != nil {
@@ -161,7 +161,7 @@ func TestValidatePayload_FormPost(t *testing.T) {
 	}
 
 	// check that if payload is invalid we get error
-	req.Header.Set(sha1SignatureHeader, "invalid signature")
+	req.Header.Set(SHA1SignatureHeader, "invalid signature")
 	if _, err = ValidatePayload(req, []byte{0}); err == nil {
 		t.Error("ValidatePayload = nil, want err")
 	}
@@ -477,6 +477,18 @@ func TestParseWebHook_BadMessageType(t *testing.T) {
 	}
 }
 
+func TestValidatePayloadFromBody_UnableToParseBody(t *testing.T) {
+	if _, err := ValidatePayloadFromBody("application/x-www-form-urlencoded", bytes.NewReader([]byte(`%`)), "sha1=", []byte{}); err == nil {
+		t.Errorf("ValidatePayloadFromBody returned nil; wanted error")
+	}
+}
+
+func TestValidatePayloadFromBody_UnsupportedContentType(t *testing.T) {
+	if _, err := ValidatePayloadFromBody("invalid", bytes.NewReader([]byte(`{}`)), "sha1=", []byte{}); err == nil {
+		t.Errorf("ValidatePayloadFromBody returned nil; wanted error")
+	}
+}
+
 func TestDeliveryID(t *testing.T) {
 	id := "8970a780-244e-11e7-91ca-da3aabcb9793"
 	req, err := http.NewRequest("POST", "http://localhost", nil)
@@ -494,7 +506,7 @@ func TestDeliveryID(t *testing.T) {
 func TestWebHookType(t *testing.T) {
 	want := "yo"
 	req := &http.Request{
-		Header: http.Header{eventTypeHeader: []string{want}},
+		Header: http.Header{EventTypeHeader: []string{want}},
 	}
 	if got := WebHookType(req); got != want {
 		t.Errorf("WebHookType = %q, want %q", got, want)
diff --git a/github/orgs_hooks_deliveries_test.go b/github/orgs_hooks_deliveries_test.go
new file mode 100644
index 00000000..c1efc527
--- /dev/null
+++ b/github/orgs_hooks_deliveries_test.go
@@ -0,0 +1,106 @@
+// Copyright 2021 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestOrganizationsService_ListHookDeliveries(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/hooks/1/deliveries", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{"cursor": "v1_12077215967"})
+		fmt.Fprint(w, `[{"id":1}, {"id":2}]`)
+	})
+
+	opt := &ListCursorOptions{Cursor: "v1_12077215967"}
+
+	ctx := context.Background()
+	hooks, _, err := client.Organizations.ListHookDeliveries(ctx, "o", 1, opt)
+	if err != nil {
+		t.Errorf("Organizations.ListHookDeliveries returned error: %v", err)
+	}
+
+	want := []*HookDelivery{{ID: Int64(1)}, {ID: Int64(2)}}
+	if d := cmp.Diff(hooks, want); d != "" {
+		t.Errorf("Organizations.ListHooks want (-), got (+):\n%s", d)
+	}
+
+	const methodName = "ListHookDeliveries"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.ListHookDeliveries(ctx, "\n", -1, opt)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.ListHookDeliveries(ctx, "o", 1, opt)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_ListHookDeliveries_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	ctx := context.Background()
+	_, _, err := client.Organizations.ListHookDeliveries(ctx, "%", 1, nil)
+	testURLParseError(t, err)
+}
+
+func TestOrganizationsService_GetHookDelivery(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/hooks/1/deliveries/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	ctx := context.Background()
+	hook, _, err := client.Organizations.GetHookDelivery(ctx, "o", 1, 1)
+	if err != nil {
+		t.Errorf("Organizations.GetHookDelivery returned error: %v", err)
+	}
+
+	want := &HookDelivery{ID: Int64(1)}
+	if !cmp.Equal(hook, want) {
+		t.Errorf("Organizations.GetHookDelivery returned %+v, want %+v", hook, want)
+	}
+
+	const methodName = "GetHookDelivery"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.GetHookDelivery(ctx, "\n", -1, -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.GetHookDelivery(ctx, "o", 1, 1)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_GetHookDelivery_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	ctx := context.Background()
+	_, _, err := client.Organizations.GetHookDelivery(ctx, "%", 1, 1)
+	testURLParseError(t, err)
+}
diff --git a/github/repos_commits_test.go b/github/repos_commits_test.go
index 430c03d6..b654b913 100644
--- a/github/repos_commits_test.go
+++ b/github/repos_commits_test.go
@@ -74,6 +74,7 @@ func TestRepositoriesService_GetCommit(t *testing.T) {
 
 	mux.HandleFunc("/repos/o/r/commits/s", func(w http.ResponseWriter, r *http.Request) {
 		testMethod(t, r, "GET")
+		testFormValues(t, r, values{"per_page": "2", "page": "2"})
 		fmt.Fprintf(w, `{
 		  "sha": "s",
 		  "commit": { "message": "m" },
@@ -97,8 +98,9 @@ func TestRepositoriesService_GetCommit(t *testing.T) {
 		}`)
 	})
 
+	opts := &ListOptions{Page: 2, PerPage: 2}
 	ctx := context.Background()
-	commit, _, err := client.Repositories.GetCommit(ctx, "o", "r", "s")
+	commit, _, err := client.Repositories.GetCommit(ctx, "o", "r", "s", opts)
 	if err != nil {
 		t.Errorf("Repositories.GetCommit returned error: %v", err)
 	}
@@ -144,12 +146,12 @@ func TestRepositoriesService_GetCommit(t *testing.T) {
 
 	const methodName = "GetCommit"
 	testBadOptions(t, methodName, func() (err error) {
-		_, _, err = client.Repositories.GetCommit(ctx, "\n", "\n", "\n")
+		_, _, err = client.Repositories.GetCommit(ctx, "\n", "\n", "\n", opts)
 		return err
 	})
 
 	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
-		got, resp, err := client.Repositories.GetCommit(ctx, "o", "r", "s")
+		got, resp, err := client.Repositories.GetCommit(ctx, "o", "r", "s", opts)
 		if got != nil {
 			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
 		}
@@ -389,6 +391,7 @@ func TestRepositoriesService_CompareCommits(t *testing.T) {
 
 		mux.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
 			testMethod(t, r, "GET")
+			testFormValues(t, r, values{"per_page": "2", "page": "2"})
 			fmt.Fprintf(w, `{
 		  "base_commit": {
 		    "sha": "s",
@@ -424,8 +427,9 @@ func TestRepositoriesService_CompareCommits(t *testing.T) {
 		}`, escapedBase, escapedHead)
 		})
 
+		opts := &ListOptions{Page: 2, PerPage: 2}
 		ctx := context.Background()
-		got, _, err := client.Repositories.CompareCommits(ctx, "o", "r", base, head)
+		got, _, err := client.Repositories.CompareCommits(ctx, "o", "r", base, head, opts)
 		if err != nil {
 			t.Errorf("Repositories.CompareCommits returned error: %v", err)
 		}
@@ -484,12 +488,12 @@ func TestRepositoriesService_CompareCommits(t *testing.T) {
 
 		const methodName = "CompareCommits"
 		testBadOptions(t, methodName, func() (err error) {
-			_, _, err = client.Repositories.CompareCommits(ctx, "\n", "\n", "\n", "\n")
+			_, _, err = client.Repositories.CompareCommits(ctx, "\n", "\n", "\n", "\n", opts)
 			return err
 		})
 
 		testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
-			got, resp, err := client.Repositories.CompareCommits(ctx, "o", "r", base, head)
+			got, resp, err := client.Repositories.CompareCommits(ctx, "o", "r", base, head, opts)
 			if got != nil {
 				t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
 			}
diff --git a/github/repos_hooks_deliveries_test.go b/github/repos_hooks_deliveries_test.go
new file mode 100644
index 00000000..5c6fb86d
--- /dev/null
+++ b/github/repos_hooks_deliveries_test.go
@@ -0,0 +1,220 @@
+// Copyright 2021 The go-github AUTHORS. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestRepositoriesService_ListHookDeliveries(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/hooks/1/deliveries", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{"cursor": "v1_12077215967"})
+		fmt.Fprint(w, `[{"id":1}, {"id":2}]`)
+	})
+
+	opt := &ListCursorOptions{Cursor: "v1_12077215967"}
+
+	ctx := context.Background()
+	hooks, _, err := client.Repositories.ListHookDeliveries(ctx, "o", "r", 1, opt)
+	if err != nil {
+		t.Errorf("Repositories.ListHookDeliveries returned error: %v", err)
+	}
+
+	want := []*HookDelivery{{ID: Int64(1)}, {ID: Int64(2)}}
+	if d := cmp.Diff(hooks, want); d != "" {
+		t.Errorf("Repositories.ListHooks want (-), got (+):\n%s", d)
+	}
+
+	const methodName = "ListHookDeliveries"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.ListHookDeliveries(ctx, "\n", "\n", -1, opt)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.ListHookDeliveries(ctx, "o", "r", 1, opt)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_ListHookDeliveries_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	ctx := context.Background()
+	_, _, err := client.Repositories.ListHookDeliveries(ctx, "%", "%", 1, nil)
+	testURLParseError(t, err)
+}
+
+func TestRepositoriesService_GetHookDelivery(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/hooks/1/deliveries/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	ctx := context.Background()
+	hook, _, err := client.Repositories.GetHookDelivery(ctx, "o", "r", 1, 1)
+	if err != nil {
+		t.Errorf("Repositories.GetHookDelivery returned error: %v", err)
+	}
+
+	want := &HookDelivery{ID: Int64(1)}
+	if !cmp.Equal(hook, want) {
+		t.Errorf("Repositories.GetHookDelivery returned %+v, want %+v", hook, want)
+	}
+
+	const methodName = "GetHookDelivery"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Repositories.GetHookDelivery(ctx, "\n", "\n", -1, -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.GetHookDelivery(ctx, "o", "r", 1, 1)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_GetHookDelivery_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	ctx := context.Background()
+	_, _, err := client.Repositories.GetHookDelivery(ctx, "%", "%", 1, 1)
+	testURLParseError(t, err)
+}
+
+var hookDeliveryPayloadTypeToStruct = map[string]interface{}{
+	"check_run":                      &CheckRunEvent{},
+	"check_suite":                    &CheckSuiteEvent{},
+	"commit_comment":                 &CommitCommentEvent{},
+	"content_reference":              &ContentReferenceEvent{},
+	"create":                         &CreateEvent{},
+	"delete":                         &DeleteEvent{},
+	"deploy_key":                     &DeployKeyEvent{},
+	"deployment":                     &DeploymentEvent{},
+	"deployment_status":              &DeploymentStatusEvent{},
+	"fork":                           &ForkEvent{},
+	"github_app_authorization":       &GitHubAppAuthorizationEvent{},
+	"gollum":                         &GollumEvent{},
+	"installation":                   &InstallationEvent{},
+	"installation_repositories":      &InstallationRepositoriesEvent{},
+	"issue_comment":                  &IssueCommentEvent{},
+	"issues":                         &IssuesEvent{},
+	"label":                          &LabelEvent{},
+	"marketplace_purchase":           &MarketplacePurchaseEvent{},
+	"member":                         &MemberEvent{},
+	"membership":                     &MembershipEvent{},
+	"meta":                           &MetaEvent{},
+	"milestone":                      &MilestoneEvent{},
+	"organization":                   &OrganizationEvent{},
+	"org_block":                      &OrgBlockEvent{},
+	"package":                        &PackageEvent{},
+	"page_build":                     &PageBuildEvent{},
+	"ping":                           &PingEvent{},
+	"project":                        &ProjectEvent{},
+	"project_card":                   &ProjectCardEvent{},
+	"project_column":                 &ProjectColumnEvent{},
+	"public":                         &PublicEvent{},
+	"pull_request":                   &PullRequestEvent{},
+	"pull_request_review":            &PullRequestReviewEvent{},
+	"pull_request_review_comment":    &PullRequestReviewCommentEvent{},
+	"pull_request_target":            &PullRequestTargetEvent{},
+	"push":                           &PushEvent{},
+	"release":                        &ReleaseEvent{},
+	"repository":                     &RepositoryEvent{},
+	"repository_dispatch":            &RepositoryDispatchEvent{},
+	"repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{},
+	"star":                           &StarEvent{},
+	"status":                         &StatusEvent{},
+	"team":                           &TeamEvent{},
+	"team_add":                       &TeamAddEvent{},
+	"user":                           &UserEvent{},
+	"watch":                          &WatchEvent{},
+	"workflow_dispatch":              &WorkflowDispatchEvent{},
+	"workflow_run":                   &WorkflowRunEvent{},
+}
+
+func TestHookDelivery_ParsePayload(t *testing.T) {
+	for evt, obj := range hookDeliveryPayloadTypeToStruct {
+		t.Run(evt, func(t *testing.T) {
+			bs, err := json.Marshal(obj)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			p := json.RawMessage(bs)
+
+			d := &HookDelivery{
+				Event: String(evt),
+				Request: &HookRequest{
+					RawPayload: &p,
+				},
+			}
+
+			got, err := d.ParseRequestPayload()
+			if err != nil {
+				t.Error(err)
+			}
+
+			if !reflect.DeepEqual(obj, got) {
+				t.Errorf("want %T %v, got %T %v", obj, obj, got, got)
+			}
+		})
+	}
+}
+
+func TestHookDelivery_ParsePayload_invalidEvent(t *testing.T) {
+	p := json.RawMessage(nil)
+
+	d := &HookDelivery{
+		Event: String("some_invalid_event"),
+		Request: &HookRequest{
+			RawPayload: &p,
+		},
+	}
+
+	_, err := d.ParseRequestPayload()
+	if err == nil || err.Error() != `unsupported event type "some_invalid_event"` {
+		t.Errorf("unexpected error: %v", err)
+	}
+}
+
+func TestHookDelivery_ParsePayload_invalidPayload(t *testing.T) {
+	p := json.RawMessage([]byte(`{"check_run":{"id":"invalid"}}`))
+
+	d := &HookDelivery{
+		Event: String("check_run"),
+		Request: &HookRequest{
+			RawPayload: &p,
+		},
+	}
+
+	_, err := d.ParseRequestPayload()
+	if err == nil || err.Error() != "json: cannot unmarshal string into Go struct field CheckRun.check_run.id of type int64" {
+		t.Errorf("unexpected error: %v", err)
+	}
+}