Repository: google/go-github
Pull request #1806 (2021-02-21 09:12:29)

Apps.ListRepos/Apps.ListUserRepos: Add mercy-preview + nebula-preview headers
**What type of PR is this?**

feature

**What this PR does / why we need it**:

* mercy-preview: For receiving repository topics
* nebula-preview: For visibility information

When calling `Apps.ListRepos` or `Apps.ListUserRepos`, the field `visibility` and `topic` are always `nil`.

The reason: Missing headers for developers preview API.

**Special notes for your reviewer**:

This applies the same fix as in https://github.com/google/go-github/pull/1780

**Does this PR introduce a user-facing change?**:

None

**Additional documentation e.g., usage docs, etc.**:

- [API changes to support internal repository visibility (2019-12-03)](https://developer.github.com/changes/2019-12-03-internal-visibility-changes/#repository-visibility-fields)


codecov[bot]: # [Codecov](https://codecov.io/gh/google/go-github/pull/1806?src=pr&el=h1) Report
> Merging [#1806](https://codecov.io/gh/google/go-github/pull/1806?src=pr&el=desc) (b535059) into [master](https://codecov.io/gh/google/go-github/commit/5c87615fe927ee848c6153a93a0bf6813a284f64?el=desc) (5c87615) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/google/go-github/pull/1806/graphs/tree.svg?width=650&height=150&src=pr&token=EgYjGjZlI0)](https://codecov.io/gh/google/go-github/pull/1806?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##           master    #1806   +/-   ##
=======================================
  Coverage   97.59%   97.59%           
=======================================
  Files         100      100           
  Lines        6521     6525    +4     
=======================================
+ Hits         6364     6368    +4     
  Misses         85       85           
  Partials       72       72           
```


| [Impacted Files](https://codecov.io/gh/google/go-github/pull/1806?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [github/apps\_installation.go](https://codecov.io/gh/google/go-github/pull/1806/diff?src=pr&el=tree#diff-Z2l0aHViL2FwcHNfaW5zdGFsbGF0aW9uLmdv) | `91.30% <100.00%> (+0.82%)` | :arrow_up: |

------

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

gmlewis: Never mind my last comment.  :joy:  See edit above.

Patch:
diff --git a/github/actions_workflow_runs.go b/github/actions_workflow_runs.go
index 4ccb7a6d..64274a3d 100644
--- a/github/actions_workflow_runs.go
+++ b/github/actions_workflow_runs.go
@@ -15,6 +15,7 @@ import (
 // WorkflowRun represents a repository action workflow run.
 type WorkflowRun struct {
 	ID             *int64         `json:"id,omitempty"`
+	Name           *string        `json:"name,omitempty"`
 	NodeID         *string        `json:"node_id,omitempty"`
 	HeadBranch     *string        `json:"head_branch,omitempty"`
 	HeadSHA        *string        `json:"head_sha,omitempty"`
diff --git a/github/apps.go b/github/apps.go
index 12df54dd..cb0069de 100644
--- a/github/apps.go
+++ b/github/apps.go
@@ -90,21 +90,26 @@ type InstallationPermissions struct {
 
 // Installation represents a GitHub Apps installation.
 type Installation struct {
-	ID                  *int64                   `json:"id,omitempty"`
-	NodeID              *string                  `json:"node_id,omitempty"`
-	AppID               *int64                   `json:"app_id,omitempty"`
-	TargetID            *int64                   `json:"target_id,omitempty"`
-	Account             *User                    `json:"account,omitempty"`
-	AccessTokensURL     *string                  `json:"access_tokens_url,omitempty"`
-	RepositoriesURL     *string                  `json:"repositories_url,omitempty"`
-	HTMLURL             *string                  `json:"html_url,omitempty"`
-	TargetType          *string                  `json:"target_type,omitempty"`
-	SingleFileName      *string                  `json:"single_file_name,omitempty"`
-	RepositorySelection *string                  `json:"repository_selection,omitempty"`
-	Events              []string                 `json:"events,omitempty"`
-	Permissions         *InstallationPermissions `json:"permissions,omitempty"`
-	CreatedAt           *Timestamp               `json:"created_at,omitempty"`
-	UpdatedAt           *Timestamp               `json:"updated_at,omitempty"`
+	ID                     *int64                   `json:"id,omitempty"`
+	NodeID                 *string                  `json:"node_id,omitempty"`
+	AppID                  *int64                   `json:"app_id,omitempty"`
+	AppSlug                *string                  `json:"app_slug,omitempty"`
+	TargetID               *int64                   `json:"target_id,omitempty"`
+	Account                *User                    `json:"account,omitempty"`
+	AccessTokensURL        *string                  `json:"access_tokens_url,omitempty"`
+	RepositoriesURL        *string                  `json:"repositories_url,omitempty"`
+	HTMLURL                *string                  `json:"html_url,omitempty"`
+	TargetType             *string                  `json:"target_type,omitempty"`
+	SingleFileName         *string                  `json:"single_file_name,omitempty"`
+	RepositorySelection    *string                  `json:"repository_selection,omitempty"`
+	Events                 []string                 `json:"events,omitempty"`
+	SingleFilePaths        []string                 `json:"single_file_paths,omitempty"`
+	Permissions            *InstallationPermissions `json:"permissions,omitempty"`
+	CreatedAt              *Timestamp               `json:"created_at,omitempty"`
+	UpdatedAt              *Timestamp               `json:"updated_at,omitempty"`
+	HasMultipleSingleFiles *bool                    `json:"has_multiple_single_files,omitempty"`
+	SuspendedBy            *User                    `json:"suspended_by,omitempty"`
+	SuspendedAt            *Timestamp               `json:"suspended_at,omitempty"`
 }
 
 // Attachment represents a GitHub Apps attachment.
diff --git a/github/apps_installation.go b/github/apps_installation.go
index b4ac5eef..501d8006 100644
--- a/github/apps_installation.go
+++ b/github/apps_installation.go
@@ -8,6 +8,7 @@ package github
 import (
 	"context"
 	"fmt"
+	"strings"
 )
 
 // ListRepositories represents the response from the list repos endpoints.
@@ -30,6 +31,10 @@ func (s *AppsService) ListRepos(ctx context.Context, opts *ListOptions) (*ListRe
 		return nil, nil, err
 	}
 
+	// TODO: remove custom Accept headers when APIs fully launch.
+	acceptHeaders := []string{mediaTypeTopicsPreview, mediaTypeRepositoryVisibilityPreview}
+	req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
+
 	var r *ListRepositories
 
 	resp, err := s.client.Do(ctx, req, &r)
@@ -56,6 +61,10 @@ func (s *AppsService) ListUserRepos(ctx context.Context, id int64, opts *ListOpt
 		return nil, nil, err
 	}
 
+	// TODO: remove custom Accept headers when APIs fully launch.
+	acceptHeaders := []string{mediaTypeTopicsPreview, mediaTypeRepositoryVisibilityPreview}
+	req.Header.Set("Accept", strings.Join(acceptHeaders, ", "))
+
 	var r *ListRepositories
 	resp, err := s.client.Do(ctx, req, &r)
 	if err != nil {
diff --git a/github/event_types.go b/github/event_types.go
index 0fa45c7c..72ac53de 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -277,16 +277,18 @@ type TeamChange struct {
 	} `json:"repository,omitempty"`
 }
 
-// InstallationEvent is triggered when a GitHub App has been installed or uninstalled.
+// InstallationEvent is triggered when a GitHub App has been installed, uninstalled, suspend, unsuspended
+// or new permissions have been accepted.
 // The Webhook event name is "installation".
 //
-// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/activity/events/types/#installationevent
+// GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/webhook-events-and-payloads#installation
 type InstallationEvent struct {
-	// The action that was performed. Can be either "created" or "deleted".
+	// The action that was performed. Can be either "created", "deleted", "suspend", "unsuspend" or "new_permissions_accepted".
 	Action       *string       `json:"action,omitempty"`
 	Repositories []*Repository `json:"repositories,omitempty"`
 	Sender       *User         `json:"sender,omitempty"`
 	Installation *Installation `json:"installation,omitempty"`
+	// TODO key "requester" is not covered
 }
 
 // InstallationRepositoriesEvent is triggered when a repository is added or
diff --git a/github/github-accessors.go b/github/github-accessors.go
index e6ea3431..88f82e31 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -4268,6 +4268,14 @@ func (i *Installation) GetAppID() int64 {
 	return *i.AppID
 }
 
+// GetAppSlug returns the AppSlug field if it's non-nil, zero value otherwise.
+func (i *Installation) GetAppSlug() string {
+	if i == nil || i.AppSlug == nil {
+		return ""
+	}
+	return *i.AppSlug
+}
+
 // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
 func (i *Installation) GetCreatedAt() Timestamp {
 	if i == nil || i.CreatedAt == nil {
@@ -4276,6 +4284,14 @@ func (i *Installation) GetCreatedAt() Timestamp {
 	return *i.CreatedAt
 }
 
+// GetHasMultipleSingleFiles returns the HasMultipleSingleFiles field if it's non-nil, zero value otherwise.
+func (i *Installation) GetHasMultipleSingleFiles() bool {
+	if i == nil || i.HasMultipleSingleFiles == nil {
+		return false
+	}
+	return *i.HasMultipleSingleFiles
+}
+
 // GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise.
 func (i *Installation) GetHTMLURL() string {
 	if i == nil || i.HTMLURL == nil {
@@ -4332,6 +4348,22 @@ func (i *Installation) GetSingleFileName() string {
 	return *i.SingleFileName
 }
 
+// GetSuspendedAt returns the SuspendedAt field if it's non-nil, zero value otherwise.
+func (i *Installation) GetSuspendedAt() Timestamp {
+	if i == nil || i.SuspendedAt == nil {
+		return Timestamp{}
+	}
+	return *i.SuspendedAt
+}
+
+// GetSuspendedBy returns the SuspendedBy field.
+func (i *Installation) GetSuspendedBy() *User {
+	if i == nil {
+		return nil
+	}
+	return i.SuspendedBy
+}
+
 // GetTargetID returns the TargetID field if it's non-nil, zero value otherwise.
 func (i *Installation) GetTargetID() int64 {
 	if i == nil || i.TargetID == nil {
@@ -15676,6 +15708,14 @@ func (w *WorkflowRun) GetLogsURL() string {
 	return *w.LogsURL
 }
 
+// GetName returns the Name field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetName() string {
+	if w == nil || w.Name == nil {
+		return ""
+	}
+	return *w.Name
+}
+
 // GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
 func (w *WorkflowRun) GetNodeID() string {
 	if w == nil || w.NodeID == nil {
diff --git a/github/repos_contents.go b/github/repos_contents.go
index 924fee2f..d88ae74b 100644
--- a/github/repos_contents.go
+++ b/github/repos_contents.go
@@ -144,6 +144,36 @@ func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo,
 	return nil, resp, fmt.Errorf("No file named %s found in %s", filename, dir)
 }
 
+// DownloadContentsWithMeta is identical to DownloadContents but additionally
+// returns the RepositoryContent of the requested file. This additional data
+// is useful for future operations involving the requested file. For merely
+// reading the content of a file, DownloadContents is perfectly adequate.
+//
+// It is possible for the download to result in a failed response when the
+// returned error is nil. Callers should check the returned Response status
+// code to verify the content is from a successful response.
+func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *RepositoryContent, *Response, error) {
+	dir := path.Dir(filepath)
+	filename := path.Base(filepath)
+	_, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts)
+	if err != nil {
+		return nil, nil, resp, err
+	}
+	for _, contents := range dirContents {
+		if *contents.Name == filename {
+			if contents.DownloadURL == nil || *contents.DownloadURL == "" {
+				return nil, contents, resp, fmt.Errorf("No download link found for %s", filepath)
+			}
+			dlResp, err := s.client.client.Get(*contents.DownloadURL)
+			if err != nil {
+				return nil, contents, &Response{Response: dlResp}, err
+			}
+			return dlResp.Body, contents, &Response{Response: dlResp}, nil
+		}
+	}
+	return nil, nil, resp, fmt.Errorf("No file named %s found in %s", filename, dir)
+}
+
 // GetContents can return either the metadata and content of a single file
 // (when path references a file) or the metadata of all the files and/or
 // subdirectories of a directory (when path references a directory). To make it
diff --git a/update-urls/main.go b/update-urls/main.go
index bdfd0882..d0d4955a 100644
--- a/update-urls/main.go
+++ b/update-urls/main.go
@@ -55,38 +55,39 @@ var (
 	// skipMethods holds methods which are skipped because they do not have GitHub v3
 	// API URLs or are otherwise problematic in parsing, discovering, and/or fixing.
 	skipMethods = map[string]bool{
-		"ActionsService.DownloadArtifact":            true,
-		"AdminService.CreateOrg":                     true,
-		"AdminService.CreateUser":                    true,
-		"AdminService.CreateUserImpersonation":       true,
-		"AdminService.DeleteUserImpersonation":       true,
-		"AdminService.GetAdminStats":                 true,
-		"AdminService.RenameOrg":                     true,
-		"AdminService.RenameOrgByName":               true,
-		"AdminService.UpdateTeamLDAPMapping":         true,
-		"AdminService.UpdateUserLDAPMapping":         true,
-		"AppsService.FindRepositoryInstallationByID": true,
-		"AuthorizationsService.CreateImpersonation":  true,
-		"AuthorizationsService.DeleteImpersonation":  true,
-		"IssueImportService.CheckStatus":             true,
-		"IssueImportService.CheckStatusSince":        true,
-		"IssueImportService.Create":                  true,
-		"MarketplaceService.marketplaceURI":          true,
-		"OrganizationsService.GetByID":               true,
-		"RepositoriesService.DeletePreReceiveHook":   true,
-		"RepositoriesService.DownloadContents":       true,
-		"RepositoriesService.GetArchiveLink":         true,
-		"RepositoriesService.GetByID":                true,
-		"RepositoriesService.GetPreReceiveHook":      true,
-		"RepositoriesService.ListPreReceiveHooks":    true,
-		"RepositoriesService.UpdatePreReceiveHook":   true,
-		"SearchService.search":                       true,
-		"TeamsService.ListTeamMembersByID":           true,
-		"UsersService.DemoteSiteAdmin":               true,
-		"UsersService.GetByID":                       true,
-		"UsersService.PromoteSiteAdmin":              true,
-		"UsersService.Suspend":                       true,
-		"UsersService.Unsuspend":                     true,
+		"ActionsService.DownloadArtifact":              true,
+		"AdminService.CreateOrg":                       true,
+		"AdminService.CreateUser":                      true,
+		"AdminService.CreateUserImpersonation":         true,
+		"AdminService.DeleteUserImpersonation":         true,
+		"AdminService.GetAdminStats":                   true,
+		"AdminService.RenameOrg":                       true,
+		"AdminService.RenameOrgByName":                 true,
+		"AdminService.UpdateTeamLDAPMapping":           true,
+		"AdminService.UpdateUserLDAPMapping":           true,
+		"AppsService.FindRepositoryInstallationByID":   true,
+		"AuthorizationsService.CreateImpersonation":    true,
+		"AuthorizationsService.DeleteImpersonation":    true,
+		"IssueImportService.CheckStatus":               true,
+		"IssueImportService.CheckStatusSince":          true,
+		"IssueImportService.Create":                    true,
+		"MarketplaceService.marketplaceURI":            true,
+		"OrganizationsService.GetByID":                 true,
+		"RepositoriesService.DeletePreReceiveHook":     true,
+		"RepositoriesService.DownloadContents":         true,
+		"RepositoriesService.DownloadContentsWithMeta": true,
+		"RepositoriesService.GetArchiveLink":           true,
+		"RepositoriesService.GetByID":                  true,
+		"RepositoriesService.GetPreReceiveHook":        true,
+		"RepositoriesService.ListPreReceiveHooks":      true,
+		"RepositoriesService.UpdatePreReceiveHook":     true,
+		"SearchService.search":                         true,
+		"TeamsService.ListTeamMembersByID":             true,
+		"UsersService.DemoteSiteAdmin":                 true,
+		"UsersService.GetByID":                         true,
+		"UsersService.PromoteSiteAdmin":                true,
+		"UsersService.Suspend":                         true,
+		"UsersService.Unsuspend":                       true,
 	}
 
 	helperOverrides = map[string]overrideFunc{


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index ef4830e9..cb8e77b3 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,7 +7,7 @@ jobs:
   test:
     strategy:
       matrix:
-        go-version: [1.x, 1.14.x]
+        go-version: [1.x, 1.15.x]
         platform: [ubuntu-latest, windows-latest]
     runs-on: ${{ matrix.platform }}
 
diff --git a/github/apps_installation_test.go b/github/apps_installation_test.go
index a73498c6..5e8a18de 100644
--- a/github/apps_installation_test.go
+++ b/github/apps_installation_test.go
@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"net/http"
 	"reflect"
+	"strings"
 	"testing"
 )
 
@@ -17,8 +18,10 @@ func TestAppsService_ListRepos(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()
 
+	wantAcceptHeaders := []string{mediaTypeTopicsPreview, mediaTypeRepositoryVisibilityPreview}
 	mux.HandleFunc("/installation/repositories", func(w http.ResponseWriter, r *http.Request) {
 		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", strings.Join(wantAcceptHeaders, ", "))
 		testFormValues(t, r, values{
 			"page":     "1",
 			"per_page": "2",
@@ -52,8 +55,10 @@ func TestAppsService_ListUserRepos(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()
 
+	wantAcceptHeaders := []string{mediaTypeTopicsPreview, mediaTypeRepositoryVisibilityPreview}
 	mux.HandleFunc("/user/installations/1/repositories", func(w http.ResponseWriter, r *http.Request) {
 		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", strings.Join(wantAcceptHeaders, ", "))
 		testFormValues(t, r, values{
 			"page":     "1",
 			"per_page": "2",
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index 5090e52e..c41e574a 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -5003,6 +5003,16 @@ func TestInstallation_GetAppID(tt *testing.T) {
 	i.GetAppID()
 }
 
+func TestInstallation_GetAppSlug(tt *testing.T) {
+	var zeroValue string
+	i := &Installation{AppSlug: &zeroValue}
+	i.GetAppSlug()
+	i = &Installation{}
+	i.GetAppSlug()
+	i = nil
+	i.GetAppSlug()
+}
+
 func TestInstallation_GetCreatedAt(tt *testing.T) {
 	var zeroValue Timestamp
 	i := &Installation{CreatedAt: &zeroValue}
@@ -5013,6 +5023,16 @@ func TestInstallation_GetCreatedAt(tt *testing.T) {
 	i.GetCreatedAt()
 }
 
+func TestInstallation_GetHasMultipleSingleFiles(tt *testing.T) {
+	var zeroValue bool
+	i := &Installation{HasMultipleSingleFiles: &zeroValue}
+	i.GetHasMultipleSingleFiles()
+	i = &Installation{}
+	i.GetHasMultipleSingleFiles()
+	i = nil
+	i.GetHasMultipleSingleFiles()
+}
+
 func TestInstallation_GetHTMLURL(tt *testing.T) {
 	var zeroValue string
 	i := &Installation{HTMLURL: &zeroValue}
@@ -5080,6 +5100,23 @@ func TestInstallation_GetSingleFileName(tt *testing.T) {
 	i.GetSingleFileName()
 }
 
+func TestInstallation_GetSuspendedAt(tt *testing.T) {
+	var zeroValue Timestamp
+	i := &Installation{SuspendedAt: &zeroValue}
+	i.GetSuspendedAt()
+	i = &Installation{}
+	i.GetSuspendedAt()
+	i = nil
+	i.GetSuspendedAt()
+}
+
+func TestInstallation_GetSuspendedBy(tt *testing.T) {
+	i := &Installation{}
+	i.GetSuspendedBy()
+	i = nil
+	i.GetSuspendedBy()
+}
+
 func TestInstallation_GetTargetID(tt *testing.T) {
 	var zeroValue int64
 	i := &Installation{TargetID: &zeroValue}
@@ -18384,6 +18421,16 @@ func TestWorkflowRun_GetLogsURL(tt *testing.T) {
 	w.GetLogsURL()
 }
 
+func TestWorkflowRun_GetName(tt *testing.T) {
+	var zeroValue string
+	w := &WorkflowRun{Name: &zeroValue}
+	w.GetName()
+	w = &WorkflowRun{}
+	w.GetName()
+	w = nil
+	w.GetName()
+}
+
 func TestWorkflowRun_GetNodeID(tt *testing.T) {
 	var zeroValue string
 	w := &WorkflowRun{NodeID: &zeroValue}
diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go
index d77747d8..8a1995d0 100644
--- a/github/github-stringify_test.go
+++ b/github/github-stringify_test.go
@@ -569,22 +569,26 @@ func TestImport_String(t *testing.T) {
 
 func TestInstallation_String(t *testing.T) {
 	v := Installation{
-		ID:                  Int64(0),
-		NodeID:              String(""),
-		AppID:               Int64(0),
-		TargetID:            Int64(0),
-		Account:             &User{},
-		AccessTokensURL:     String(""),
-		RepositoriesURL:     String(""),
-		HTMLURL:             String(""),
-		TargetType:          String(""),
-		SingleFileName:      String(""),
-		RepositorySelection: String(""),
-		Permissions:         &InstallationPermissions{},
-		CreatedAt:           &Timestamp{},
-		UpdatedAt:           &Timestamp{},
-	}
-	want := `github.Installation{ID:0, NodeID:"", AppID:0, TargetID:0, Account:github.User{}, AccessTokensURL:"", RepositoriesURL:"", HTMLURL:"", TargetType:"", SingleFileName:"", RepositorySelection:"", Permissions:github.InstallationPermissions{}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}`
+		ID:                     Int64(0),
+		NodeID:                 String(""),
+		AppID:                  Int64(0),
+		AppSlug:                String(""),
+		TargetID:               Int64(0),
+		Account:                &User{},
+		AccessTokensURL:        String(""),
+		RepositoriesURL:        String(""),
+		HTMLURL:                String(""),
+		TargetType:             String(""),
+		SingleFileName:         String(""),
+		RepositorySelection:    String(""),
+		Permissions:            &InstallationPermissions{},
+		CreatedAt:              &Timestamp{},
+		UpdatedAt:              &Timestamp{},
+		HasMultipleSingleFiles: Bool(false),
+		SuspendedBy:            &User{},
+		SuspendedAt:            &Timestamp{},
+	}
+	want := `github.Installation{ID:0, NodeID:"", AppID:0, AppSlug:"", TargetID:0, Account:github.User{}, AccessTokensURL:"", RepositoriesURL:"", HTMLURL:"", TargetType:"", SingleFileName:"", RepositorySelection:"", Permissions:github.InstallationPermissions{}, CreatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, UpdatedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, HasMultipleSingleFiles:false, SuspendedBy:github.User{}, SuspendedAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}}`
 	if got := v.String(); got != want {
 		t.Errorf("Installation.String = %v, want %v", got, want)
 	}
diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go
index afc260b3..c8cfb45c 100644
--- a/github/repos_contents_test.go
+++ b/github/repos_contents_test.go
@@ -247,6 +247,155 @@ func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) {
 	}
 }
 
+func TestRepositoriesService_DownloadContentsWithMeta_Success(t *testing.T) {
+	client, mux, serverURL, teardown := setup()
+	defer teardown()
+	mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[{
+		  "type": "file",
+		  "name": "f",
+		  "download_url": "`+serverURL+baseURLPath+`/download/f"
+		}]`)
+	})
+	mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, "foo")
+	})
+
+	ctx := context.Background()
+	r, c, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil)
+	if err != nil {
+		t.Errorf("Repositories.DownloadContentsWithMeta returned error: %v", err)
+	}
+
+	if got, want := resp.Response.StatusCode, http.StatusOK; got != want {
+		t.Errorf("Repositories.DownloadContentsWithMeta returned status code %v, want %v", got, want)
+	}
+
+	bytes, err := ioutil.ReadAll(r)
+	if err != nil {
+		t.Errorf("Error reading response body: %v", err)
+	}
+	r.Close()
+
+	if got, want := string(bytes), "foo"; got != want {
+		t.Errorf("Repositories.DownloadContentsWithMeta returned %v, want %v", got, want)
+	}
+
+	if c != nil && c.Name != nil {
+		if got, want := *c.Name, "f"; got != want {
+			t.Errorf("Repositories.DownloadContentsWithMeta returned content name %v, want %v", got, want)
+		}
+	} else {
+		t.Errorf("Returned RepositoryContent is null")
+	}
+
+	const methodName = "DownloadContentsWithMeta"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, _, err = client.Repositories.DownloadContentsWithMeta(ctx, "\n", "\n", "\n", nil)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, cot, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		if cot != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, cot)
+		}
+		return resp, err
+	})
+}
+
+func TestRepositoriesService_DownloadContentsWithMeta_FailedResponse(t *testing.T) {
+	client, mux, serverURL, teardown := setup()
+	defer teardown()
+	mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[{
+			"type": "file",
+			"name": "f",
+			"download_url": "`+serverURL+baseURLPath+`/download/f"
+		  }]`)
+	})
+	mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		w.WriteHeader(http.StatusInternalServerError)
+		fmt.Fprint(w, "foo error")
+	})
+
+	ctx := context.Background()
+	r, c, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil)
+	if err != nil {
+		t.Errorf("Repositories.DownloadContentsWithMeta returned error: %v", err)
+	}
+
+	if got, want := resp.Response.StatusCode, http.StatusInternalServerError; got != want {
+		t.Errorf("Repositories.DownloadContentsWithMeta returned status code %v, want %v", got, want)
+	}
+
+	bytes, err := ioutil.ReadAll(r)
+	if err != nil {
+		t.Errorf("Error reading response body: %v", err)
+	}
+	r.Close()
+
+	if got, want := string(bytes), "foo error"; got != want {
+		t.Errorf("Repositories.DownloadContentsWithMeta returned %v, want %v", got, want)
+	}
+
+	if c != nil && c.Name != nil {
+		if got, want := *c.Name, "f"; got != want {
+			t.Errorf("Repositories.DownloadContentsWithMeta returned content name %v, want %v", got, want)
+		}
+	} else {
+		t.Errorf("Returned RepositoryContent is null")
+	}
+}
+
+func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+	mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[{
+		  "type": "file",
+		  "name": "f",
+		}]`)
+	})
+
+	ctx := context.Background()
+	_, _, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil)
+	if err == nil {
+		t.Errorf("Repositories.DownloadContentsWithMeta did not return expected error")
+	}
+
+	if resp == nil {
+		t.Errorf("Repositories.DownloadContentsWithMeta did not return expected response")
+	}
+}
+
+func TestRepositoriesService_DownloadContentsWithMeta_NoFile(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+	mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `[]`)
+	})
+
+	ctx := context.Background()
+	_, _, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil)
+	if err == nil {
+		t.Errorf("Repositories.DownloadContentsWithMeta did not return expected error")
+	}
+
+	if resp == nil {
+		t.Errorf("Repositories.DownloadContentsWithMeta did not return expected response")
+	}
+}
+
 func TestRepositoriesService_GetContents_File(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()