Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support directory retrieval in Prow's Github client #20461

Merged
merged 3 commits into from
Jan 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 42 additions & 4 deletions prow/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ type RepositoryClient interface {
AddLabel(org, repo string, number int, label string) error
RemoveLabel(org, repo string, number int, label string) error
GetFile(org, repo, filepath, commit string) ([]byte, error)
GetDirectory(org, repo, dirpath, commit string) ([]DirectoryContent, error)
IsCollaborator(org, repo, user string) (bool, error)
ListCollaborators(org, repo string) ([]User, error)
CreateFork(owner, repo string) (string, error)
Expand Down Expand Up @@ -2998,22 +2999,22 @@ func (e *FileNotFound) Error() string {

// GetFile uses GitHub repo contents API to retrieve the content of a file with commit SHA.
// If commit is empty, it will grab content from repo's default branch, usually master.
// TODO(krzyzacy): Support retrieve a directory
// Use GetDirectory() method to retrieve a directory.
//
// See https://developer.github.com/v3/repos/contents/#get-contents
func (c *client) GetFile(org, repo, filepath, commit string) ([]byte, error) {
durationLogger := c.log("GetFile", org, repo, filepath, commit)
defer durationLogger()

url := fmt.Sprintf("/repos/%s/%s/contents/%s", org, repo, filepath)
path := fmt.Sprintf("/repos/%s/%s/contents/%s", org, repo, filepath)
if commit != "" {
url = fmt.Sprintf("%s?ref=%s", url, commit)
path = fmt.Sprintf("%s?ref=%s", path, url.QueryEscape(commit))
}

var res Content
code, err := c.request(&request{
method: http.MethodGet,
path: url,
path: path,
org: org,
exitCodes: []int{200, 404},
}, &res)
Expand Down Expand Up @@ -4168,3 +4169,40 @@ func (c *client) GetApp() (*App, error) {

return &app, nil
}

// GetDirectory uses GitHub repo contents API to retrieve the content of a directory with commit SHA.
// If commit is empty, it will grab content from repo's default branch, usually master.
//
// See https://developer.github.com/v3/repos/contents/#get-contents
func (c *client) GetDirectory(org, repo, dirpath, commit string) ([]DirectoryContent, error) {
durationLogger := c.log("GetDirectory", org, repo, dirpath, commit)
defer durationLogger()

path := fmt.Sprintf("/repos/%s/%s/contents/%s", org, repo, dirpath)
if commit != "" {
path = fmt.Sprintf("%s?ref=%s", path, url.QueryEscape(commit))
}
rahulbharuka marked this conversation as resolved.
Show resolved Hide resolved

var res []DirectoryContent
code, err := c.request(&request{
method: http.MethodGet,
path: path,
org: org,
exitCodes: []int{200, 404},
}, &res)

if err != nil {
return nil, err
}

if code == 404 {
return nil, &FileNotFound{
org: org,
repo: repo,
path: dirpath,
commit: commit,
}
}

return res, nil
}
82 changes: 82 additions & 0 deletions prow/github/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2681,3 +2681,85 @@ func TestV4ClientSetsUserAgent(t *testing.T) {
}
})
}

func TestGetDirectory(t *testing.T) {
expectedContents := []DirectoryContent{
{
Type: "file",
Name: "bar",
Path: "foo/bar",
},
{
Type: "dir",
Name: "hello",
Path: "foo/hello",
},
}
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Bad method: %s", r.Method)
}
if r.URL.Path != "/repos/k8s/kuber/contents/foo" {
t.Errorf("Bad request path: %s", r.URL.Path)
}
if r.URL.RawQuery != "" {
t.Errorf("Bad request query: %s", r.URL.RawQuery)
}
b, err := json.Marshal(&expectedContents)
if err != nil {
t.Fatalf("Didn't expect error: %v", err)
}
fmt.Fprint(w, string(b))
}))
defer ts.Close()
c := getClient(ts.URL)
if contents, err := c.GetDirectory("k8s", "kuber", "foo", ""); err != nil {
t.Errorf("Didn't expect error: %v", err)
} else if len(contents) != 2 {
t.Errorf("Expected two contents, found %d: %v", len(contents), contents)
return
} else if !reflect.DeepEqual(contents, expectedContents) {
t.Errorf("Wrong list of teams, expected: %v, got: %v", expectedContents, contents)
}
}

func TestGetDirectoryRef(t *testing.T) {
expectedContents := []DirectoryContent{
{
Type: "file",
Name: "bar.go",
Path: "foo/bar.go",
},
{
Type: "dir",
Name: "hello",
Path: "foo/hello",
},
}
ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
t.Errorf("Bad method: %s", r.Method)
}
if r.URL.Path != "/repos/k8s/kuber/contents/foo" {
t.Errorf("Bad request path: %s", r.URL.Path)
}
if r.URL.RawQuery != "ref=12345" {
t.Errorf("Bad request query: %s", r.URL.RawQuery)
}
b, err := json.Marshal(&expectedContents)
if err != nil {
t.Fatalf("Didn't expect error: %v", err)
}
fmt.Fprint(w, string(b))
}))
defer ts.Close()
c := getClient(ts.URL)
if contents, err := c.GetDirectory("k8s", "kuber", "foo", "12345"); err != nil {
t.Errorf("Didn't expect error: %v", err)
} else if len(contents) != 2 {
t.Errorf("Expected two contents, found %d: %v", len(contents), contents)
return
} else if !reflect.DeepEqual(contents, expectedContents) {
t.Errorf("Wrong list of teams, expected: %v, got: %v", expectedContents, contents)
}
}
25 changes: 25 additions & 0 deletions prow/github/fakegithub/fakegithub.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ type FakeClient struct {
// and values map SHA to content
RemoteFiles map[string]map[string]string

// Fake remote git storage. Directory name are keys
// and values map SHA to directory content
RemoteDirectories map[string]map[string][]github.DirectoryContent

// A list of refs that got deleted via DeleteRef
RefsDeleted []struct{ Org, Repo, Ref string }

Expand Down Expand Up @@ -758,3 +762,24 @@ func (f *FakeClient) UpdatePullRequest(org, repo string, number int, title, body
func (f *FakeClient) Query(ctx context.Context, q interface{}, vars map[string]interface{}) error {
return nil
}

// GetDirectory returns the contents of the file.
func (f *FakeClient) GetDirectory(org, repo, dir, commit string) ([]github.DirectoryContent, error) {
contents, ok := f.RemoteDirectories[dir]
if !ok {
return nil, fmt.Errorf("could not find dir %s", dir)
}
if commit == "" {
if master, ok := contents["master"]; ok {
return master, nil
}

return nil, fmt.Errorf("could not find dir %s in master", dir)
}

if content, ok := contents[commit]; ok {
return content, nil
}

return nil, fmt.Errorf("could not find dir %s with ref %s", dir, commit)
}
14 changes: 14 additions & 0 deletions prow/github/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,9 @@ type DraftReviewComment struct {
}

// Content is some base64 encoded github file content
rahulbharuka marked this conversation as resolved.
Show resolved Hide resolved
// It include selected fields available in content record returned by
// GH "GET" method. See also:
// https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-repository-content
type Content struct {
Content string `json:"content"`
SHA string `json:"sha"`
Expand Down Expand Up @@ -1325,3 +1328,14 @@ type AppInstallationToken struct {
Permissions InstallationPermissions `json:"permissions,omitempty"`
Repositories []Repo `json:"repositories,omitempty"`
}

// DirectoryContent contains information about a github directory.
// It include selected fields available in content records returned by
// GH "GET" method. See also:
// https://docs.github.com/en/free-pro-team@latest/rest/reference/repos#get-repository-content
type DirectoryContent struct {
rahulbharuka marked this conversation as resolved.
Show resolved Hide resolved
SHA string `json:"sha"`
Type string `json:"type"`
Name string `json:"name"`
Path string `json:"path"`
}