Repository: google/go-github (go)

Issue:
List/Get/Redeliver App Webhook Deliveries
Any plans to support the additional APIs around App Webhook Deliveries? https://docs.github.com/en/rest/reference/apps#list-deliveries-for-an-app-webhook

I noticed the functionality around App Webhook configuration was implemented already

Pull request:
Add app hook deliveries API
Resolves #2225 

Patch:
diff --git a/.gitignore b/.gitignore
index aea73858..c903cf6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@ coverage.out
 .idea/
 vendor/
 .DS_Store
-.vscode
\ No newline at end of file
+.vscode
+# vim temp files
+*.swp
diff --git a/github/apps_hooks_deliveries.go b/github/apps_hooks_deliveries.go
new file mode 100644
index 00000000..0b631b80
--- /dev/null
+++ b/github/apps_hooks_deliveries.go
@@ -0,0 +1,72 @@
+// 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 deliveries of an App webhook.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/apps#list-deliveries-for-an-app-webhook
+func (s *AppsService) ListHookDeliveries(ctx context.Context, opts *ListCursorOptions) ([]*HookDelivery, *Response, error) {
+	u, err := addOptions("app/hook/deliveries", 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 the App webhook delivery with the specified ID.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/apps#get-a-delivery-for-an-app-webhook
+func (s *AppsService) GetHookDelivery(ctx context.Context, deliveryID int64) (*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("app/hook/deliveries/%v", 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
+}
+
+// RedeliverHookDelivery redelivers a delivery for an App webhook.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/apps#redeliver-a-delivery-for-an-app-webhook
+func (s *AppsService) RedeliverHookDelivery(ctx context.Context, deliveryID int64) (*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("app/hook/deliveries/%v/attempts", deliveryID)
+	req, err := s.client.NewRequest("POST", 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/doc.go b/github/doc.go
index d67e2869..3bf67d7d 100644
--- a/github/doc.go
+++ b/github/doc.go
@@ -125,12 +125,16 @@ from the most recent API call. If a recent enough response isn't
 available, you can use RateLimits to fetch the most up-to-date rate
 limit data for the client.
 
-To detect an API rate limit error, you can check if its type is *github.RateLimitError:
+To detect an API rate limit error, you can check if its type is *github.RateLimitError.
+For secondary rate limits, you can check if its type is *github.AbuseRateLimitError:
 
 	repos, _, err := client.Repositories.List(ctx, "", nil)
 	if _, ok := err.(*github.RateLimitError); ok {
 		log.Println("hit rate limit")
 	}
+	if _, ok := err.(*github.AbuseRateLimitError); ok {
+		log.Println("hit secondary rate limit")
+	}
 
 Learn more about GitHub rate limiting at
 https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#rate-limiting.
diff --git a/github/event_types.go b/github/event_types.go
index f63c8040..122a7da9 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -230,12 +230,13 @@ type GollumEvent struct {
 	Installation *Installation `json:"installation,omitempty"`
 }
 
-// EditChange represents the changes when an issue, pull request, or comment has
-// been edited.
+// EditChange represents the changes when an issue, pull request, comment,
+// or repository has been edited.
 type EditChange struct {
 	Title *EditTitle `json:"title,omitempty"`
 	Body  *EditBody  `json:"body,omitempty"`
 	Base  *EditBase  `json:"base,omitempty"`
+	Repo  *EditRepo  `json:"repository,omitempty"`
 }
 
 // EditTitle represents a pull-request title change.
@@ -259,6 +260,16 @@ type EditRef struct {
 	From *string `json:"from,omitempty"`
 }
 
+// EditRepo represents a change of repository name.
+type EditRepo struct {
+	Name *RepoName `json:"name,omitempty"`
+}
+
+// RepoName represents a change of repository name.
+type RepoName struct {
+	From *string `json:"from,omitempty"`
+}
+
 // EditSHA represents a sha change of a pull-request.
 type EditSHA struct {
 	From *string `json:"from,omitempty"`
@@ -938,6 +949,7 @@ type RepositoryEvent struct {
 	Repo   *Repository `json:"repository,omitempty"`
 
 	// The following fields are only populated by Webhook events.
+	Changes      *EditChange   `json:"changes,omitempty"`
 	Org          *Organization `json:"organization,omitempty"`
 	Sender       *User         `json:"sender,omitempty"`
 	Installation *Installation `json:"installation,omitempty"`
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 166e2ad5..189e68b3 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -4100,6 +4100,14 @@ func (e *EditChange) GetBody() *EditBody {
 	return e.Body
 }
 
+// GetRepo returns the Repo field.
+func (e *EditChange) GetRepo() *EditRepo {
+	if e == nil {
+		return nil
+	}
+	return e.Repo
+}
+
 // GetTitle returns the Title field.
 func (e *EditChange) GetTitle() *EditTitle {
 	if e == nil {
@@ -4116,6 +4124,14 @@ func (e *EditRef) GetFrom() string {
 	return *e.From
 }
 
+// GetName returns the Name field.
+func (e *EditRepo) GetName() *RepoName {
+	if e == nil {
+		return nil
+	}
+	return e.Name
+}
+
 // GetFrom returns the From field if it's non-nil, zero value otherwise.
 func (e *EditSHA) GetFrom() string {
 	if e == nil || e.From == nil {
@@ -13196,6 +13212,14 @@ func (r *RenameOrgResponse) GetURL() string {
 	return *r.URL
 }
 
+// GetFrom returns the From field if it's non-nil, zero value otherwise.
+func (r *RepoName) GetFrom() string {
+	if r == nil || r.From == nil {
+		return ""
+	}
+	return *r.From
+}
+
 // GetIncompleteResults returns the IncompleteResults field if it's non-nil, zero value otherwise.
 func (r *RepositoriesSearchResult) GetIncompleteResults() bool {
 	if r == nil || r.IncompleteResults == nil {
@@ -14332,6 +14356,14 @@ func (r *RepositoryEvent) GetAction() string {
 	return *r.Action
 }
 
+// GetChanges returns the Changes field.
+func (r *RepositoryEvent) GetChanges() *EditChange {
+	if r == nil {
+		return nil
+	}
+	return r.Changes
+}
+
 // GetInstallation returns the Installation field.
 func (r *RepositoryEvent) GetInstallation() *Installation {
 	if r == nil {
diff --git a/github/github.go b/github/github.go
index ee3bb0ab..26bf78a9 100644
--- a/github/github.go
+++ b/github/github.go
@@ -884,7 +884,7 @@ func (ae *AcceptedError) Is(target error) bool {
 }
 
 // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
-// "documentation_url" field value equal to "https://docs.github.com/en/free-pro-team@latest/rest/reference/#abuse-rate-limits".
+// "documentation_url" field value equal to "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#secondary-rate-limits".
 type AbuseRateLimitError struct {
 	Response *http.Response // HTTP response that caused this error
 	Message  string         `json:"message"` // error message
@@ -1003,7 +1003,9 @@ func CheckResponse(r *http.Response) error {
 			Response: errorResponse.Response,
 			Message:  errorResponse.Message,
 		}
-	case r.StatusCode == http.StatusForbidden && strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits"):
+	case r.StatusCode == http.StatusForbidden &&
+		(strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") ||
+			strings.HasSuffix(errorResponse.DocumentationURL, "#secondary-rate-limits")):
 		abuseRateLimitError := &AbuseRateLimitError{
 			Response: errorResponse.Response,
 			Message:  errorResponse.Message,
diff --git a/github/pulls.go b/github/pulls.go
index 6c866ea8..37fb7413 100644
--- a/github/pulls.go
+++ b/github/pulls.go
@@ -167,9 +167,10 @@ func (s *PullRequestsService) List(ctx context.Context, owner string, repo strin
 
 // ListPullRequestsWithCommit returns pull requests associated with a commit SHA.
 //
-// The results will include open and closed pull requests.
+// The results may include open and closed pull requests.
+// By default, the PullRequestListOptions State filters for "open".
 //
-// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#list-pull-requests-associated-with-a-commit
+// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/commits/#list-pull-requests-associated-with-a-commit
 func (s *PullRequestsService) ListPullRequestsWithCommit(ctx context.Context, owner, repo, sha string, opts *PullRequestListOptions) ([]*PullRequest, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/commits/%v/pulls", owner, repo, sha)
 	u, err := addOptions(u, opts)


Test patch:
diff --git a/github/apps_hooks_deliveries_test.go b/github/apps_hooks_deliveries_test.go
new file mode 100644
index 00000000..38e25540
--- /dev/null
+++ b/github/apps_hooks_deliveries_test.go
@@ -0,0 +1,119 @@
+// 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 TestAppsService_ListHookDeliveries(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/hook/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}]`)
+	})
+
+	opts := &ListCursorOptions{Cursor: "v1_12077215967"}
+
+	ctx := context.Background()
+
+	deliveries, _, err := client.Apps.ListHookDeliveries(ctx, opts)
+	if err != nil {
+		t.Errorf("Apps.ListHookDeliveries returned error: %v", err)
+	}
+
+	want := []*HookDelivery{{ID: Int64(1)}, {ID: Int64(2)}}
+	if d := cmp.Diff(deliveries, want); d != "" {
+		t.Errorf("Apps.ListHooks want (-), got (+):\n%s", d)
+	}
+
+	const methodName = "ListHookDeliveries"
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Apps.ListHookDeliveries(ctx, opts)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestAppsService_GetHookDelivery(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/hook/deliveries/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	ctx := context.Background()
+	hook, _, err := client.Apps.GetHookDelivery(ctx, 1)
+	if err != nil {
+		t.Errorf("Apps.GetHookDelivery returned error: %v", err)
+	}
+
+	want := &HookDelivery{ID: Int64(1)}
+	if !cmp.Equal(hook, want) {
+		t.Errorf("Apps.GetHookDelivery returned %+v, want %+v", hook, want)
+	}
+
+	const methodName = "GetHookDelivery"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Apps.GetHookDelivery(ctx, -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Apps.GetHookDelivery(ctx, 1)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestAppsService_RedeliverHookDelivery(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/hook/deliveries/1/attempts", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	ctx := context.Background()
+	hook, _, err := client.Apps.RedeliverHookDelivery(ctx, 1)
+	if err != nil {
+		t.Errorf("Apps.RedeliverHookDelivery returned error: %v", err)
+	}
+
+	want := &HookDelivery{ID: Int64(1)}
+	if !cmp.Equal(hook, want) {
+		t.Errorf("Apps.RedeliverHookDelivery returned %+v, want %+v", hook, want)
+	}
+
+	const methodName = "RedeliverHookDelivery"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Apps.RedeliverHookDelivery(ctx, -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Apps.RedeliverHookDelivery(ctx, 1)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
diff --git a/github/event_types_test.go b/github/event_types_test.go
index d7113b55..bb5dd40d 100644
--- a/github/event_types_test.go
+++ b/github/event_types_test.go
@@ -82,6 +82,28 @@ func TestEditChange_Marshal_BaseChange(t *testing.T) {
 	testJSONMarshal(t, u, want)
 }
 
+func TestEditChange_Marshal_Repo(t *testing.T) {
+	testJSONMarshal(t, &EditChange{}, "{}")
+
+	u := &EditChange{
+		Repo: &EditRepo{
+			Name: &RepoName{
+				From: String("old-repo-name"),
+			},
+		},
+	}
+
+	want := `{
+		"repository": {
+			"name": {
+				"from": "old-repo-name"
+			}
+		}
+	}`
+
+	testJSONMarshal(t, u, want)
+}
+
 func TestProjectChange_Marshal_NameChange(t *testing.T) {
 	testJSONMarshal(t, &ProjectChange{}, "{}")
 
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index fa4dc56c..69ed4030 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -4796,6 +4796,13 @@ func TestEditChange_GetBody(tt *testing.T) {
 	e.GetBody()
 }
 
+func TestEditChange_GetRepo(tt *testing.T) {
+	e := &EditChange{}
+	e.GetRepo()
+	e = nil
+	e.GetRepo()
+}
+
 func TestEditChange_GetTitle(tt *testing.T) {
 	e := &EditChange{}
 	e.GetTitle()
@@ -4813,6 +4820,13 @@ func TestEditRef_GetFrom(tt *testing.T) {
 	e.GetFrom()
 }
 
+func TestEditRepo_GetName(tt *testing.T) {
+	e := &EditRepo{}
+	e.GetName()
+	e = nil
+	e.GetName()
+}
+
 func TestEditSHA_GetFrom(tt *testing.T) {
 	var zeroValue string
 	e := &EditSHA{From: &zeroValue}
@@ -15374,6 +15388,16 @@ func TestRenameOrgResponse_GetURL(tt *testing.T) {
 	r.GetURL()
 }
 
+func TestRepoName_GetFrom(tt *testing.T) {
+	var zeroValue string
+	r := &RepoName{From: &zeroValue}
+	r.GetFrom()
+	r = &RepoName{}
+	r.GetFrom()
+	r = nil
+	r.GetFrom()
+}
+
 func TestRepositoriesSearchResult_GetIncompleteResults(tt *testing.T) {
 	var zeroValue bool
 	r := &RepositoriesSearchResult{IncompleteResults: &zeroValue}
@@ -16731,6 +16755,13 @@ func TestRepositoryEvent_GetAction(tt *testing.T) {
 	r.GetAction()
 }
 
+func TestRepositoryEvent_GetChanges(tt *testing.T) {
+	r := &RepositoryEvent{}
+	r.GetChanges()
+	r = nil
+	r.GetChanges()
+}
+
 func TestRepositoryEvent_GetInstallation(tt *testing.T) {
 	r := &RepositoryEvent{}
 	r.GetInstallation()