From e66fe34cde5162a62a2afa8e69e9b11f6581458d Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Tue, 14 Apr 2026 16:51:30 +0100 Subject: [PATCH 1/9] feat: Refactor repositories download contents Signed-off-by: Steve Hipwell --- github/repos_contents.go | 85 ++++------- github/repos_contents_test.go | 258 ++++++++++------------------------ 2 files changed, 101 insertions(+), 242 deletions(-) diff --git a/github/repos_contents.go b/github/repos_contents.go index 0c2ac3bf25f..c54176f49c5 100644 --- a/github/repos_contents.go +++ b/github/repos_contents.go @@ -17,7 +17,6 @@ import ( "io" "net/http" "net/url" - "path" "strings" ) @@ -137,40 +136,8 @@ func (s *RepositoriesService) GetReadme(ctx context.Context, owner, repo string, // //meta:operation GET /repos/{owner}/{repo}/contents/{path} func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, filepath string, opts *RepositoryContentGetOptions) (io.ReadCloser, *Response, error) { - dir := path.Dir(filepath) - filename := path.Base(filepath) - fileContent, _, resp, err := s.GetContents(ctx, owner, repo, filepath, opts) - if err == nil && fileContent != nil { - content, err := fileContent.GetContent() - if err == nil && content != "" { - return io.NopCloser(strings.NewReader(content)), resp, nil - } - } - - _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) - if err != nil { - return nil, resp, err - } - - for _, contents := range dirContents { - if contents.GetName() == filename { - if contents.GetDownloadURL() == "" { - return nil, resp, fmt.Errorf("no download link found for %v", filepath) - } - dlReq, err := http.NewRequestWithContext(ctx, "GET", *contents.DownloadURL, nil) - if err != nil { - return nil, resp, err - } - dlResp, err := s.client.client.Do(dlReq) - if err != nil { - return nil, &Response{Response: dlResp}, err - } - - return dlResp.Body, &Response{Response: dlResp}, nil - } - } - - return nil, resp, fmt.Errorf("no file named %v found in %v", filename, dir) + rc, _, resp, err := s.DownloadContentsWithMeta(ctx, owner, repo, filepath, opts) + return rc, resp, err } // DownloadContentsWithMeta is identical to DownloadContents but additionally @@ -186,40 +153,36 @@ func (s *RepositoriesService) DownloadContents(ctx context.Context, owner, repo, // //meta:operation GET /repos/{owner}/{repo}/contents/{path} 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) fileContent, _, resp, err := s.GetContents(ctx, owner, repo, filepath, opts) - if err == nil && fileContent != nil { - content, err := fileContent.GetContent() - if err == nil && content != "" { - return io.NopCloser(strings.NewReader(content)), fileContent, resp, nil - } + if err != nil { + return nil, nil, resp, err + } + + if fileContent == nil { + return nil, nil, resp, fmt.Errorf("no file content found at path %v in %v/%v", filepath, owner, repo) + } + + content, err := fileContent.GetContent() + if err == nil && content != "" { + return io.NopCloser(strings.NewReader(content)), fileContent, resp, nil } - _, dirContents, resp, err := s.GetContents(ctx, owner, repo, dir, opts) + downloadURL := fileContent.GetDownloadURL() + if downloadURL == "" { + return nil, fileContent, resp, fmt.Errorf("could not get download url for path %v in %v/%v", filepath, owner, repo) + } + + dlReq, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) if err != nil { - return nil, nil, resp, err + return nil, fileContent, resp, err } - for _, contents := range dirContents { - if contents.GetName() == filename { - if contents.GetDownloadURL() == "" { - return nil, contents, resp, fmt.Errorf("no download link found for %v", filepath) - } - dlReq, err := http.NewRequestWithContext(ctx, "GET", *contents.DownloadURL, nil) - if err != nil { - return nil, contents, resp, err - } - dlResp, err := s.client.client.Do(dlReq) - if err != nil { - return nil, contents, &Response{Response: dlResp}, err - } - - return dlResp.Body, contents, &Response{Response: dlResp}, nil - } + dlResp, err := s.client.client.Do(dlReq) + if err != nil { + return nil, fileContent, &Response{Response: dlResp}, err } - return nil, nil, resp, fmt.Errorf("no file named %v found in %v", filename, dir) + return dlResp.Body, fileContent, &Response{Response: dlResp}, nil } // GetContents can return either the metadata and content of a single file diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go index 113aad6ebc4..7527677973f 100644 --- a/github/repos_contents_test.go +++ b/github/repos_contents_test.go @@ -126,77 +126,17 @@ func TestRepositoriesService_GetReadme(t *testing.T) { }) } -func TestRepositoriesService_DownloadContents_SuccessForFile(t *testing.T) { +func TestRepositoriesService_DownloadContents_Success(t *testing.T) { t.Parallel() - client, mux, serverURL := setup(t) - - mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "content": "foo", - "download_url": "`+serverURL+baseURLPath+`/download/f" - }`) - }) - - ctx := t.Context() - r, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) - if err != nil { - t.Errorf("Repositories.DownloadContents returned error: %v", err) - } - - if got, want := resp.Response.StatusCode, http.StatusOK; got != want { - t.Errorf("Repositories.DownloadContents returned status code %v, want %v", got, want) - } - - bytes, err := io.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.DownloadContents returned %v, want %v", got, want) - } - - const methodName = "DownloadContents" - testBadOptions(t, methodName, func() (err error) { - _, _, err = client.Repositories.DownloadContents(ctx, "\n", "\n", "\n", nil) - return err - }) - - testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { - got, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) - if got != nil { - t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) - } - return resp, err - }) -} - -func TestRepositoriesService_DownloadContents_SuccessForDirectory(t *testing.T) { - t.Parallel() - client, mux, serverURL := setup(t) + client, mux, _ := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "type": "file", - "name": "f" - }`) - }) - 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") + "type": "file", + "name": "f", + "content": "foo" +}`) }) ctx := t.Context() @@ -234,52 +174,6 @@ func TestRepositoriesService_DownloadContents_SuccessForDirectory(t *testing.T) }) } -func TestRepositoriesService_DownloadContents_FailedResponse(t *testing.T) { - t.Parallel() - client, mux, serverURL := setup(t) - - mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f" - }`) - }) - 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 := t.Context() - r, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) - if err != nil { - t.Errorf("Repositories.DownloadContents returned error: %v", err) - } - - if got, want := resp.Response.StatusCode, http.StatusInternalServerError; got != want { - t.Errorf("Repositories.DownloadContents returned status code %v, want %v", got, want) - } - - bytes, err := io.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.DownloadContents returned %v, want %v", got, want) - } -} - func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -287,18 +181,9 @@ func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "content": "" - }`) - }) - 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", - "content": "" - }]`) + "type": "file", + "name": "f" +}`) }) ctx := t.Context() @@ -312,26 +197,17 @@ func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { } if reader != nil { - t.Error("Repositories.DownloadContents did not return expected reader") + t.Error("Repositories.DownloadContents returned unexpected reader") } } -func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) { +func TestRepositoriesService_DownloadContents_GetContentsError(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "content": "" - }`) - }) - - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { - testMethod(t, r, "GET") - fmt.Fprint(w, `[]`) + w.WriteHeader(http.StatusNotFound) }) ctx := t.Context() @@ -345,22 +221,21 @@ func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) { } if reader != nil { - t.Error("Repositories.DownloadContents did not return expected reader") + t.Error("Repositories.DownloadContents returned unexpected reader") } } -func TestRepositoriesService_DownloadContentsWithMeta_SuccessForFile(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_SuccessWithContent(t *testing.T) { t.Parallel() - client, mux, serverURL := setup(t) + client, mux, _ := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "download_url": "`+serverURL+baseURLPath+`/download/f", - "content": "foo" - }`) + "type": "file", + "name": "f", + "content": "foo" +}`) }) ctx := t.Context() @@ -409,17 +284,17 @@ func TestRepositoriesService_DownloadContentsWithMeta_SuccessForFile(t *testing. }) } -func TestRepositoriesService_DownloadContentsWithMeta_SuccessForDirectory(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_SuccessViaDownloadURL(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) - mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `[{ - "type": "file", - "name": "f", - "download_url": "`+serverURL+baseURLPath+`/download/f" - }]`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") @@ -459,29 +334,19 @@ func TestRepositoriesService_DownloadContentsWithMeta_FailedResponse(t *testing. t.Parallel() client, mux, serverURL := setup(t) - downloadURL := fmt.Sprintf("%v%v/download/f", serverURL, baseURLPath) - mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ - "type": "file", - "name": "f", - "download_url": "`+downloadURL+`" - }`) + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "foo error") }) - 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": "`+downloadURL+`" - }]`) - }) ctx := t.Context() r, c, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil) @@ -519,17 +384,9 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, `{ - "type": "file", - "name": "f", - }`) - }) - 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", - "content": "" - }]`) + "type": "file", + "name": "f" +}`) }) ctx := t.Context() @@ -539,7 +396,7 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T } if reader != nil { - t.Error("Repositories.DownloadContentsWithMeta did not return expected reader") + t.Error("Repositories.DownloadContentsWithMeta returned unexpected reader") } if resp == nil { @@ -551,24 +408,63 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T } } -func TestRepositoriesService_DownloadContentsWithMeta_NoFile(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_GetContentsError(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + w.WriteHeader(http.StatusNotFound) + }) + + ctx := t.Context() + reader, contents, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil) + if err == nil { + t.Error("Repositories.DownloadContentsWithMeta did not return expected error") + } + + if reader != nil { + t.Error("Repositories.DownloadContentsWithMeta returned unexpected reader") + } + + if resp == nil { + t.Error("Repositories.DownloadContentsWithMeta did not return expected response") + } + + if contents != nil { + t.Error("Repositories.DownloadContentsWithMeta returned unexpected content") + } +} + +func TestRepositoriesService_DownloadContentsWithMeta_NilFileContent(t *testing.T) { t.Parallel() client, mux, _ := setup(t) mux.HandleFunc("/repos/o/r/contents/d", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `[]`) + fmt.Fprint(w, `[{ + "type": "file", + "name": "f" +}]`) }) ctx := t.Context() - _, _, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil) + reader, contents, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d", nil) if err == nil { t.Error("Repositories.DownloadContentsWithMeta did not return expected error") } + if reader != nil { + t.Error("Repositories.DownloadContentsWithMeta returned unexpected reader") + } + if resp == nil { t.Error("Repositories.DownloadContentsWithMeta did not return expected response") } + + if contents != nil { + t.Error("Repositories.DownloadContentsWithMeta returned unexpected content") + } } func TestRepositoriesService_GetContents_File(t *testing.T) { From 55c3ae6be1dcba13713be5b8d9f1c832e5313dc9 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Tue, 14 Apr 2026 17:30:32 +0100 Subject: [PATCH 2/9] fixup! feat: Refactor repositories download contents --- tools/metadata/main_test.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tools/metadata/main_test.go b/tools/metadata/main_test.go index 703c2d1d7cb..7951bce6b63 100644 --- a/tools/metadata/main_test.go +++ b/tools/metadata/main_test.go @@ -362,13 +362,12 @@ func newTestServer(t *testing.T, ref string, files map[string]any) *httptest.Ser descriptionsContent = append(descriptionsContent, &github.RepositoryContent{ Name: github.Ptr(path.Base(path.Dir(name))), }) + dlURL := server.URL + "/dl/" + name mux.HandleFunc( - path.Join(repoPath, "contents/descriptions", path.Dir(name)), - jsonHandler(refQuery, []*github.RepositoryContent{ - { - Name: github.Ptr(path.Base(name)), - DownloadURL: github.Ptr(server.URL + "/dl/" + name), - }, + path.Join(repoPath, "contents/descriptions", name), + jsonHandler(refQuery, &github.RepositoryContent{ + Name: github.Ptr(path.Base(name)), + DownloadURL: github.Ptr(dlURL), }), ) mux.HandleFunc( From c0dca2a90a71f083774f9efe2ddec2039e4e4591 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Wed, 15 Apr 2026 08:39:47 +0100 Subject: [PATCH 3/9] fixup! feat: Refactor repositories download contents --- tools/metadata/main_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/metadata/main_test.go b/tools/metadata/main_test.go index 7951bce6b63..4cb2436cb60 100644 --- a/tools/metadata/main_test.go +++ b/tools/metadata/main_test.go @@ -367,7 +367,7 @@ func newTestServer(t *testing.T, ref string, files map[string]any) *httptest.Ser path.Join(repoPath, "contents/descriptions", name), jsonHandler(refQuery, &github.RepositoryContent{ Name: github.Ptr(path.Base(name)), - DownloadURL: github.Ptr(dlURL), + DownloadURL: &dlURL, }), ) mux.HandleFunc( From ba15a1c74f9c1212fd46bc113abbd355f905e7c6 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Wed, 15 Apr 2026 12:46:03 +0100 Subject: [PATCH 4/9] fixup! feat: Refactor repositories download contents --- tools/metadata/main_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/metadata/main_test.go b/tools/metadata/main_test.go index 4cb2436cb60..e9b656190a6 100644 --- a/tools/metadata/main_test.go +++ b/tools/metadata/main_test.go @@ -362,12 +362,11 @@ func newTestServer(t *testing.T, ref string, files map[string]any) *httptest.Ser descriptionsContent = append(descriptionsContent, &github.RepositoryContent{ Name: github.Ptr(path.Base(path.Dir(name))), }) - dlURL := server.URL + "/dl/" + name mux.HandleFunc( path.Join(repoPath, "contents/descriptions", name), jsonHandler(refQuery, &github.RepositoryContent{ Name: github.Ptr(path.Base(name)), - DownloadURL: &dlURL, + DownloadURL: github.Ptr(server.URL + "/dl/" + name), }), ) mux.HandleFunc( From 7b15366c2d8838c1707b92997e6da04019180e1f Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 16 Apr 2026 10:41:57 +0100 Subject: [PATCH 5/9] fixup! feat: Refactor repositories download contents Signed-off-by: Steve Hipwell --- example/contents/main.go | 69 ++++++++++++++ github/repos_contents_test.go | 174 +++++++++++++++++++++++----------- 2 files changed, 186 insertions(+), 57 deletions(-) create mode 100644 example/contents/main.go diff --git a/example/contents/main.go b/example/contents/main.go new file mode 100644 index 00000000000..d4b14c6efba --- /dev/null +++ b/example/contents/main.go @@ -0,0 +1,69 @@ +// Copyright 2026 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. + +// The commitpr command utilizes go-github as a CLI tool for +// pushing files to a branch and creating a pull request from it. +// It takes an auth token as an environment variable and creates +// the commit and the PR under the account affiliated with that token. +// +// The purpose of this example is to show how to use refs, trees and commits to +// create commits and pull requests. +// +// Note, if you want to push a single file, you probably prefer to use the +// content API. An example is available here: +// https://pkg.go.dev/github.com/google/go-github/v84/github#example-RepositoriesService-CreateFile +// +// Note, for this to work at least 1 commit is needed, so you if you use this +// after creating a repository you might want to make sure you set `AutoInit` to +// `true`. +package main + +import ( + "context" + "fmt" + "io" + "os" + + "github.com/google/go-github/v84/github" +) + +// downloadContents downloads the contents of a file in a repository and returns it as a byte slice. +func downloadContents(ctx context.Context, client *github.Client, owner, repo, path, ref string) ([]byte, error) { + rc, _, err := client.Repositories.DownloadContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{Ref: ref}) + if err != nil { + return nil, err + } + defer rc.Close() + + by, err := io.ReadAll(rc) + if err != nil { + return nil, err + } + + fmt.Printf("Downloaded %v/%v/%v as %d bytes\n", owner, repo, path, len(by)) + return by, nil +} + +func main() { + client := github.NewClient(nil) + + t := []struct { + owner string + repo string + path string + ref string + }{ + {"google", "go-github", "README.md", "master"}, + {"github", "rest-api-description", "descriptions/api.github.com/api.github.com.2026-03-10.yaml", "main"}, + {"ScoopInstaller", "Main", "bucket/yq.json", "master"}, + } + + for _, v := range t { + if _, err := downloadContents(context.Background(), client, v.owner, v.repo, v.path, v.ref); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + } +} diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go index 7527677973f..3211acf615b 100644 --- a/github/repos_contents_test.go +++ b/github/repos_contents_test.go @@ -126,17 +126,72 @@ func TestRepositoriesService_GetReadme(t *testing.T) { }) } -func TestRepositoriesService_DownloadContents_Success(t *testing.T) { +func TestRepositoriesService_DownloadContents_SuccessWithContent(t *testing.T) { t.Parallel() - client, mux, _ := setup(t) + client, mux, serverURL := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ + fmt.Fprintf(w, `{ "type": "file", "name": "f", - "content": "foo" -}`) + "content": "foo", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) + }) + + ctx := t.Context() + r, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) + if err != nil { + t.Errorf("Repositories.DownloadContents returned error: %v", err) + } + + if got, want := resp.Response.StatusCode, http.StatusOK; got != want { + t.Errorf("Repositories.DownloadContents returned status code %v, want %v", got, want) + } + + bytes, err := io.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.DownloadContents returned %v, want %v", got, want) + } + + const methodName = "DownloadContents" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Repositories.DownloadContents(ctx, "\n", "\n", "\n", nil) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestRepositoriesService_DownloadContents_SuccessByDownload(t *testing.T) { + t.Parallel() + client, mux, serverURL := setup(t) + + mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "content": "", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) + }) + + mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, "foo") }) ctx := t.Context() @@ -174,6 +229,46 @@ func TestRepositoriesService_DownloadContents_Success(t *testing.T) { }) } +func TestRepositoriesService_DownloadContents_FailedResponse(t *testing.T) { + t.Parallel() + client, mux, serverURL := setup(t) + + mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprintf(w, `{ + "type": "file", + "name": "f", + "content": "", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) + }) + 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 := t.Context() + r, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d/f", nil) + if err != nil { + t.Errorf("Repositories.DownloadContents returned error: %v", err) + } + + if got, want := resp.Response.StatusCode, http.StatusInternalServerError; got != want { + t.Errorf("Repositories.DownloadContents returned status code %v, want %v", got, want) + } + + bytes, err := io.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.DownloadContents returned %v, want %v", got, want) + } +} + func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -182,7 +277,8 @@ func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { testMethod(t, r, "GET") fmt.Fprint(w, `{ "type": "file", - "name": "f" + "name": "f", + "content": "" }`) }) @@ -197,11 +293,11 @@ func TestRepositoriesService_DownloadContents_NoDownloadURL(t *testing.T) { } if reader != nil { - t.Error("Repositories.DownloadContents returned unexpected reader") + t.Error("Repositories.DownloadContents did not return expected reader") } } -func TestRepositoriesService_DownloadContents_GetContentsError(t *testing.T) { +func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -221,21 +317,22 @@ func TestRepositoriesService_DownloadContents_GetContentsError(t *testing.T) { } if reader != nil { - t.Error("Repositories.DownloadContents returned unexpected reader") + t.Error("Repositories.DownloadContents did not return expected reader") } } func TestRepositoriesService_DownloadContentsWithMeta_SuccessWithContent(t *testing.T) { t.Parallel() - client, mux, _ := setup(t) + client, mux, serverURL := setup(t) mux.HandleFunc("/repos/o/r/contents/d/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - fmt.Fprint(w, `{ + fmt.Fprintf(w, `{ "type": "file", "name": "f", - "content": "foo" -}`) + "content": "foo", + "download_url": "%v/download/f" +}`, serverURL+baseURLPath) }) ctx := t.Context() @@ -284,7 +381,7 @@ func TestRepositoriesService_DownloadContentsWithMeta_SuccessWithContent(t *test }) } -func TestRepositoriesService_DownloadContentsWithMeta_SuccessViaDownloadURL(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_SuccessByDownload(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) @@ -296,6 +393,7 @@ func TestRepositoriesService_DownloadContentsWithMeta_SuccessViaDownloadURL(t *t "download_url": "%v/download/f" }`, serverURL+baseURLPath) }) + mux.HandleFunc("/download/f", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") fmt.Fprint(w, "foo") @@ -385,7 +483,8 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T testMethod(t, r, "GET") fmt.Fprint(w, `{ "type": "file", - "name": "f" + "name": "f", + "content": "" }`) }) @@ -396,7 +495,7 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T } if reader != nil { - t.Error("Repositories.DownloadContentsWithMeta returned unexpected reader") + t.Error("Repositories.DownloadContentsWithMeta did not return expected reader") } if resp == nil { @@ -408,7 +507,7 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoDownloadURL(t *testing.T } } -func TestRepositoriesService_DownloadContentsWithMeta_GetContentsError(t *testing.T) { +func TestRepositoriesService_DownloadContentsWithMeta_NoFile(t *testing.T) { t.Parallel() client, mux, _ := setup(t) @@ -418,53 +517,14 @@ func TestRepositoriesService_DownloadContentsWithMeta_GetContentsError(t *testin }) ctx := t.Context() - reader, contents, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil) - if err == nil { - t.Error("Repositories.DownloadContentsWithMeta did not return expected error") - } - - if reader != nil { - t.Error("Repositories.DownloadContentsWithMeta returned unexpected reader") - } - - if resp == nil { - t.Error("Repositories.DownloadContentsWithMeta did not return expected response") - } - - if contents != nil { - t.Error("Repositories.DownloadContentsWithMeta returned unexpected content") - } -} - -func TestRepositoriesService_DownloadContentsWithMeta_NilFileContent(t *testing.T) { - t.Parallel() - client, mux, _ := setup(t) - - 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 := t.Context() - reader, contents, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d", nil) + _, _, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d/f", nil) if err == nil { t.Error("Repositories.DownloadContentsWithMeta did not return expected error") } - if reader != nil { - t.Error("Repositories.DownloadContentsWithMeta returned unexpected reader") - } - if resp == nil { t.Error("Repositories.DownloadContentsWithMeta did not return expected response") } - - if contents != nil { - t.Error("Repositories.DownloadContentsWithMeta returned unexpected content") - } } func TestRepositoriesService_GetContents_File(t *testing.T) { From d25a1ae99aa21223b31feab2850a19c2cf9a4048 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 16 Apr 2026 13:40:17 +0100 Subject: [PATCH 6/9] fixup! feat: Refactor repositories download contents --- example/contents/main.go | 91 +++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 43 deletions(-) diff --git a/example/contents/main.go b/example/contents/main.go index d4b14c6efba..086549b0ecc 100644 --- a/example/contents/main.go +++ b/example/contents/main.go @@ -3,67 +3,72 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// The commitpr command utilizes go-github as a CLI tool for -// pushing files to a branch and creating a pull request from it. -// It takes an auth token as an environment variable and creates -// the commit and the PR under the account affiliated with that token. -// -// The purpose of this example is to show how to use refs, trees and commits to -// create commits and pull requests. -// -// Note, if you want to push a single file, you probably prefer to use the -// content API. An example is available here: -// https://pkg.go.dev/github.com/google/go-github/v84/github#example-RepositoriesService-CreateFile -// -// Note, for this to work at least 1 commit is needed, so you if you use this -// after creating a repository you might want to make sure you set `AutoInit` to -// `true`. +// The contents command utilizes go-github as a CLI tool for +// downloading the contents of a file in a repository. +// It takes an inputs of the repository owner, repository name, path to the +// file in the repository, reference (branch, tag or commit SHA), and output +// path for the downloaded file. It then uses the Repositories.DownloadContents +// method to download the file and saves it to the specified output path. package main import ( + "bufio" "context" "fmt" "io" "os" + "strings" "github.com/google/go-github/v84/github" ) -// downloadContents downloads the contents of a file in a repository and returns it as a byte slice. -func downloadContents(ctx context.Context, client *github.Client, owner, repo, path, ref string) ([]byte, error) { - rc, _, err := client.Repositories.DownloadContents(ctx, owner, repo, path, &github.RepositoryContentGetOptions{Ref: ref}) +func main() { + fmt.Println("This example will download the contents of a file from a GitHub repository.") + + r := bufio.NewReader(os.Stdin) + + fmt.Print("Repository Owner: ") + owner, _ := r.ReadString('\n') + owner = strings.TrimSpace(owner) + + fmt.Print("Repository Name: ") + repo, _ := r.ReadString('\n') + repo = strings.TrimSpace(repo) + + fmt.Print("Repository Path: ") + repoPath, _ := r.ReadString('\n') + repoPath = strings.TrimSpace(repoPath) + + fmt.Print("Reference (branch, tag or commit SHA): ") + ref, _ := r.ReadString('\n') + ref = strings.TrimSpace(ref) + + fmt.Print("Output Path: ") + outputPath, _ := r.ReadString('\n') + outputPath = strings.TrimSpace(outputPath) + + fmt.Printf("\nDownloading %v/%v/%v at ref %v to %v...\n", owner, repo, repoPath, ref, outputPath) + + client := github.NewClient(nil) + + rc, _, err := client.Repositories.DownloadContents(context.Background(), owner, repo, repoPath, &github.RepositoryContentGetOptions{Ref: ref}) if err != nil { - return nil, err + fmt.Printf("Error: %v\n", err) + os.Exit(1) } defer rc.Close() - by, err := io.ReadAll(rc) + f, err := os.Create(outputPath) if err != nil { - return nil, err + fmt.Printf("Error: %v\n", err) + os.Exit(1) } + defer f.Close() - fmt.Printf("Downloaded %v/%v/%v as %d bytes\n", owner, repo, path, len(by)) - return by, nil -} - -func main() { - client := github.NewClient(nil) - - t := []struct { - owner string - repo string - path string - ref string - }{ - {"google", "go-github", "README.md", "master"}, - {"github", "rest-api-description", "descriptions/api.github.com/api.github.com.2026-03-10.yaml", "main"}, - {"ScoopInstaller", "Main", "bucket/yq.json", "master"}, + if _, err := io.Copy(f, rc); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) } - for _, v := range t { - if _, err := downloadContents(context.Background(), client, v.owner, v.repo, v.path, v.ref); err != nil { - fmt.Printf("Error: %v\n", err) - os.Exit(1) - } - } + fmt.Println("Download completed.") } From 52d75be59d86adb9d607442272e55405d540353d Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 16 Apr 2026 16:06:35 +0100 Subject: [PATCH 7/9] fixup! feat: Refactor repositories download contents Signed-off-by: Steve Hipwell --- example/contents/main.go | 5 ++-- github/repos_contents_test.go | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/example/contents/main.go b/example/contents/main.go index 086549b0ecc..7749f094edf 100644 --- a/example/contents/main.go +++ b/example/contents/main.go @@ -17,6 +17,7 @@ import ( "fmt" "io" "os" + "path/filepath" "strings" "github.com/google/go-github/v84/github" @@ -45,7 +46,7 @@ func main() { fmt.Print("Output Path: ") outputPath, _ := r.ReadString('\n') - outputPath = strings.TrimSpace(outputPath) + outputPath = filepath.Clean(strings.TrimSpace(outputPath)) fmt.Printf("\nDownloading %v/%v/%v at ref %v to %v...\n", owner, repo, repoPath, ref, outputPath) @@ -58,7 +59,7 @@ func main() { } defer rc.Close() - f, err := os.Create(outputPath) + f, err := os.Create(outputPath) //#nosec G703 -- path is validated above if err != nil { fmt.Printf("Error: %v\n", err) os.Exit(1) diff --git a/github/repos_contents_test.go b/github/repos_contents_test.go index 3211acf615b..bd0f1562779 100644 --- a/github/repos_contents_test.go +++ b/github/repos_contents_test.go @@ -321,6 +321,34 @@ func TestRepositoriesService_DownloadContents_NoFile(t *testing.T) { } } +func TestRepositoriesService_DownloadContents_NotFile(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + 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", + "content": "" +}]`) + }) + + ctx := t.Context() + reader, resp, err := client.Repositories.DownloadContents(ctx, "o", "r", "d", nil) + if err == nil { + t.Error("Repositories.DownloadContents did not return expected error") + } + + if resp == nil { + t.Error("Repositories.DownloadContents did not return expected response") + } + + if reader != nil { + t.Error("Repositories.DownloadContents did not return expected reader") + } +} + func TestRepositoriesService_DownloadContentsWithMeta_SuccessWithContent(t *testing.T) { t.Parallel() client, mux, serverURL := setup(t) @@ -527,6 +555,30 @@ func TestRepositoriesService_DownloadContentsWithMeta_NoFile(t *testing.T) { } } +func TestRepositoriesService_DownloadContentsWithMeta_NotFile(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + 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", + "content": "" +}]`) + }) + + ctx := t.Context() + _, _, resp, err := client.Repositories.DownloadContentsWithMeta(ctx, "o", "r", "d", nil) + if err == nil { + t.Error("Repositories.DownloadContentsWithMeta did not return expected error") + } + + if resp == nil { + t.Error("Repositories.DownloadContentsWithMeta did not return expected response") + } +} + func TestRepositoriesService_GetContents_File(t *testing.T) { t.Parallel() client, mux, _ := setup(t) From 77fd8f25146f511b3968d61cb3ac0acc2a960b55 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 17 Apr 2026 14:15:57 +0100 Subject: [PATCH 8/9] fixup! feat: Refactor repositories download contents --- github/repos_contents.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/repos_contents.go b/github/repos_contents.go index c54176f49c5..2c5a9152168 100644 --- a/github/repos_contents.go +++ b/github/repos_contents.go @@ -159,7 +159,7 @@ func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owne } if fileContent == nil { - return nil, nil, resp, fmt.Errorf("no file content found at path %v in %v/%v", filepath, owner, repo) + return nil, nil, resp, fmt.Errorf("no file content found") } content, err := fileContent.GetContent() @@ -169,7 +169,7 @@ func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owne downloadURL := fileContent.GetDownloadURL() if downloadURL == "" { - return nil, fileContent, resp, fmt.Errorf("could not get download url for path %v in %v/%v", filepath, owner, repo) + return nil, fileContent, resp, fmt.Errorf("download url is empty") } dlReq, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil) From 45817d91935b53ff21b53ec3ee993ace4a492b17 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Fri, 17 Apr 2026 14:21:33 +0100 Subject: [PATCH 9/9] fixup! feat: Refactor repositories download contents --- github/repos_contents.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/github/repos_contents.go b/github/repos_contents.go index 2c5a9152168..53313ea22da 100644 --- a/github/repos_contents.go +++ b/github/repos_contents.go @@ -159,7 +159,7 @@ func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owne } if fileContent == nil { - return nil, nil, resp, fmt.Errorf("no file content found") + return nil, nil, resp, errors.New("no file content found") } content, err := fileContent.GetContent() @@ -169,7 +169,7 @@ func (s *RepositoriesService) DownloadContentsWithMeta(ctx context.Context, owne downloadURL := fileContent.GetDownloadURL() if downloadURL == "" { - return nil, fileContent, resp, fmt.Errorf("download url is empty") + return nil, fileContent, resp, errors.New("download url is empty") } dlReq, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)