Browse Source

Added code for the Content API

With this code it should be possible to tick the ‘Content’ box in issue
#37. The code is written after reviewing PR #64 and so is (very)
loosely based on that PR. So big thanks for the work already done by
@octavore! I did however had some different insights about a few fixes
and as the PR hasn’t moved for the last 5 months, I thought to just
rewrite and PR and I send in a new PR hoping to come to a merge…
Sander van Harmelen 12 years ago
committed by Will Norris
parent
commit
290b4791fb
2 changed files with 356 additions and 14 deletions
  1. +158
    -1
      github/repos_contents.go
  2. +198
    -13
      github/repos_contents_test.go

+ 158
- 1
github/repos_contents.go View File

@ -10,8 +10,11 @@ package github
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
)
// RepositoryContent represents a file or directory in a github repository.
@ -28,6 +31,28 @@ type RepositoryContent struct {
HTMLURL *string `json:"htmlurl,omitempty"`
}
// RepositoryContentResponse holds the parsed response from CreateFile, UpdateFile, and DeleteFile.
type RepositoryContentResponse struct {
Content *RepositoryContent `json:"content,omitempty"`
Commit `json:"commit,omitempty"`
}
// RepositoryContentFileOptions specifies optional parameters for CreateFile, UpdateFile, and DeleteFile.
type RepositoryContentFileOptions struct {
Message *string `json:"message,omitempty"`
Content []byte `json:"content,omitempty"`
SHA *string `json:"sha,omitempty"`
Branch *string `json:"branch,omitempty"`
Author *CommitAuthor `json:"author,omitempty"`
Committer *CommitAuthor `json:"committer,omitempty"`
}
// RepositoryContentGetOptions represents an optional ref parameter, which can be a SHA,
// branch, or tag
type RepositoryContentGetOptions struct {
Ref string `url:"ref,omitempty"`
}
func (r RepositoryContent) String() string {
return Stringify(r)
}
@ -47,8 +72,12 @@ func (c *RepositoryContent) Decode() ([]byte, error) {
// GetReadme gets the Readme file for the repository.
//
// GitHub API docs: http://developer.github.com/v3/repos/contents/#get-the-readme
func (s *RepositoriesService) GetReadme(owner, repo string) (*RepositoryContent, *Response, error) {
func (s *RepositoriesService) GetReadme(owner, repo string, opt *RepositoryContentGetOptions) (*RepositoryContent, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/readme", owner, repo)
u, err := addOptions(u, opt)
if err != nil {
return nil, nil, err
}
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
@ -60,3 +89,131 @@ func (s *RepositoriesService) GetReadme(owner, repo string) (*RepositoryContent,
}
return readme, resp, err
}
// 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
// easy to distinguish between both result types and to mimic the API as much
// as possible, both result types will be returned but only one will contain a
// value and the other will be nil.
//
// GitHub API docs: http://developer.github.com/v3/repos/contents/#get-contents
func (s *RepositoriesService) GetContents(owner, repo, path string, opt *RepositoryContentGetOptions) (fileContent *RepositoryContent,
directoryContent []*RepositoryContent, resp *Response, err error) {
u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
u, err = addOptions(u, opt)
if err != nil {
return nil, nil, nil, err
}
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, nil, err
}
var rawJSON json.RawMessage
resp, err = s.client.Do(req, &rawJSON)
if err != nil {
return nil, nil, resp, err
}
fileUnmarshalError := json.Unmarshal(rawJSON, &fileContent)
if fileUnmarshalError == nil {
return fileContent, nil, resp, fileUnmarshalError
}
directoryUnmarshalError := json.Unmarshal(rawJSON, &directoryContent)
if directoryUnmarshalError == nil {
return nil, directoryContent, resp, directoryUnmarshalError
}
return nil, nil, resp, fmt.Errorf("Unmarshalling failed for both file and directory content: %s and %s ", fileUnmarshalError, directoryUnmarshalError)
}
// CreateFile creates a new file in a repository at the given path and returns
// the commit and file metadata.
//
// GitHub API docs: http://developer.github.com/v3/repos/contents/#create-a-file
func (s *RepositoriesService) CreateFile(owner, repo, path string, opt *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
req, err := s.client.NewRequest("PUT", u, opt)
if err != nil {
return nil, nil, err
}
createResponse := new(RepositoryContentResponse)
resp, err := s.client.Do(req, createResponse)
if err != nil {
return nil, resp, err
}
return createResponse, resp, err
}
// UpdateFile updates a file in a repository at the given path and returns the
// commit and file metadata. Requires the blob SHA of the file being updated.
//
// GitHub API docs: http://developer.github.com/v3/repos/contents/#update-a-file
func (s *RepositoriesService) UpdateFile(owner, repo, path string, opt *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
req, err := s.client.NewRequest("PUT", u, opt)
if err != nil {
return nil, nil, err
}
updateResponse := new(RepositoryContentResponse)
resp, err := s.client.Do(req, updateResponse)
if err != nil {
return nil, resp, err
}
return updateResponse, resp, err
}
// DeleteFile deletes a file from a repository and returns the commit.
// Requires the blob SHA of the file to be deleted.
//
// GitHub API docs: http://developer.github.com/v3/repos/contents/#delete-a-file
func (s *RepositoriesService) DeleteFile(owner, repo, path string, opt *RepositoryContentFileOptions) (*RepositoryContentResponse, *Response, error) {
u := fmt.Sprintf("repos/%s/%s/contents/%s", owner, repo, path)
req, err := s.client.NewRequest("DELETE", u, opt)
if err != nil {
return nil, nil, err
}
deleteResponse := new(RepositoryContentResponse)
resp, err := s.client.Do(req, deleteResponse)
if err != nil {
return nil, resp, err
}
return deleteResponse, resp, err
}
// archiveFormat is used to define the archive type when calling GetArchiveLink.
type archiveFormat string
const (
// Tarball specifies an archive in gzipped tar format.
Tarball archiveFormat = "tarball"
// Zipball specifies an archive in zip format.
Zipball archiveFormat = "zipball"
)
// GetArchiveLink returns an URL to download a tarball or zipball archive for a
// repository. The archiveFormat can be specified by either the github.Tarball
// or github.Zipball constant.
//
// GitHub API docs: http://developer.github.com/v3/repos/contents/#get-archive-link
func (s *RepositoriesService) GetArchiveLink(owner, repo string, archiveformat archiveFormat, opt *RepositoryContentGetOptions) (*url.URL, *Response, error) {
u := fmt.Sprintf("repos/%s/%s/%s", owner, repo, archiveformat)
if opt != nil && opt.Ref != "" {
u += fmt.Sprintf("/%s", opt.Ref)
}
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
var resp *http.Response
// Use http.DefaultTransport if no custom Transport is configured
if s.client.client.Transport == nil {
resp, err = http.DefaultTransport.RoundTrip(req)
} else {
resp, err = s.client.client.Transport.RoundTrip(req)
}
if err != nil || resp.StatusCode != http.StatusFound {
return nil, newResponse(resp), err
}
parsedURL, err := url.Parse(resp.Header.Get("Location"))
return parsedURL, newResponse(resp), err
}

+ 198
- 13
github/repos_contents_test.go View File

@ -7,6 +7,30 @@ import (
"testing"
)
func TestDecode(t *testing.T) {
setup()
defer teardown()
r := RepositoryContent{Encoding: String("base64"), Content: String("aGVsbG8=")}
o, err := r.Decode()
if err != nil {
t.Errorf("Failed to decode content.")
}
want := "hello"
if string(o) != want {
t.Errorf("RepositoryContent.Decode returned %+v, want %+v", string(o), want)
}
}
func TestDecodeBadEncoding(t *testing.T) {
setup()
defer teardown()
r := RepositoryContent{Encoding: String("bad")}
_, err := r.Decode()
if err == nil {
t.Errorf("Should fail to decode non-base64")
}
}
func TestRepositoriesService_GetReadme(t *testing.T) {
setup()
defer teardown()
@ -20,7 +44,7 @@ func TestRepositoriesService_GetReadme(t *testing.T) {
"path": "README.md"
}`)
})
readme, _, err := client.Repositories.GetReadme("o", "r")
readme, _, err := client.Repositories.GetReadme("o", "r", &RepositoryContentGetOptions{})
if err != nil {
t.Errorf("Repositories.GetReadme returned error: %v", err)
}
@ -30,26 +54,187 @@ func TestRepositoriesService_GetReadme(t *testing.T) {
}
}
func TestDecodeBadEncoding(t *testing.T) {
func TestRepositoriesService_GetContent_File(t *testing.T) {
setup()
defer teardown()
r := RepositoryContent{Encoding: String("bad")}
_, err := r.Decode()
if err == nil {
t.Errorf("Should fail to decode non-base64")
mux.HandleFunc("/repos/o/r/contents/p", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{
"type": "file",
"encoding": "base64",
"size": 20678,
"name": "LICENSE",
"path": "LICENSE"
}`)
})
fileContents, _, _, err := client.Repositories.GetContents("o", "r", "p", &RepositoryContentGetOptions{})
if err != nil {
t.Errorf("Repositories.GetContents_File returned error: %v", err)
}
want := &RepositoryContent{Type: String("file"), Name: String("LICENSE"), Size: Int(20678), Encoding: String("base64"), Path: String("LICENSE")}
if !reflect.DeepEqual(fileContents, want) {
t.Errorf("Repositories.GetContents returned %+v, want %+v", fileContents, want)
}
}
func TestDecode(t *testing.T) {
func TestRepositoriesService_GetContent_Directory(t *testing.T) {
setup()
defer teardown()
r := RepositoryContent{Encoding: String("base64"), Content: String("aGVsbG8=")}
o, err := r.Decode()
mux.HandleFunc("/repos/o/r/contents/p", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `[{
"type": "dir",
"name": "lib",
"path": "lib"
},
{
"type": "file",
"size": 20678,
"name": "LICENSE",
"path": "LICENSE"
}]`)
})
_, directoryContents, _, err := client.Repositories.GetContents("o", "r", "p", &RepositoryContentGetOptions{})
if err != nil {
t.Errorf("Failed to decode content.")
t.Errorf("Repositories.GetContents_Directory returned error: %v", err)
}
want := "hello"
if string(o) != want {
t.Errorf("RepositoryContent.Decode returned %+v, want %+v", string(o), want)
want := []*RepositoryContent{{Type: String("dir"), Name: String("lib"), Path: String("lib")},
{Type: String("file"), Name: String("LICENSE"), Size: Int(20678), Path: String("LICENSE")}}
if !reflect.DeepEqual(directoryContents, want) {
t.Errorf("Repositories.GetContents_Directory returned %+v, want %+v", directoryContents, want)
}
}
func TestRepositoriesService_CreateFile(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/contents/p", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "PUT")
fmt.Fprint(w, `{
"content":{
"name":"p"
},
"commit":{
"message":"m",
"sha":"f5f369044773ff9c6383c087466d12adb6fa0828"
}
}`)
})
message := "m"
content := []byte("c")
repositoryContentsOptions := &RepositoryContentFileOptions{
Message: &message,
Content: content,
Committer: &CommitAuthor{Name: String("n"), Email: String("e")},
}
createResponse, _, err := client.Repositories.CreateFile("o", "r", "p", repositoryContentsOptions)
if err != nil {
t.Errorf("Repositories.CreateFile returned error: %v", err)
}
want := &RepositoryContentResponse{
Content: &RepositoryContent{Name: String("p")},
Commit: Commit{
Message: String("m"),
SHA: String("f5f369044773ff9c6383c087466d12adb6fa0828"),
},
}
if !reflect.DeepEqual(createResponse, want) {
t.Errorf("Repositories.CreateFile returned %+v, want %+v", createResponse, want)
}
}
func TestRepositoriesService_UpdateFile(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/contents/p", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "PUT")
fmt.Fprint(w, `{
"content":{
"name":"p"
},
"commit":{
"message":"m",
"sha":"f5f369044773ff9c6383c087466d12adb6fa0828"
}
}`)
})
message := "m"
content := []byte("c")
sha := "f5f369044773ff9c6383c087466d12adb6fa0828"
repositoryContentsOptions := &RepositoryContentFileOptions{
Message: &message,
Content: content,
SHA: &sha,
Committer: &CommitAuthor{Name: String("n"), Email: String("e")},
}
updateResponse, _, err := client.Repositories.UpdateFile("o", "r", "p", repositoryContentsOptions)
if err != nil {
t.Errorf("Repositories.UpdateFile returned error: %v", err)
}
want := &RepositoryContentResponse{
Content: &RepositoryContent{Name: String("p")},
Commit: Commit{
Message: String("m"),
SHA: String("f5f369044773ff9c6383c087466d12adb6fa0828"),
},
}
if !reflect.DeepEqual(updateResponse, want) {
t.Errorf("Repositories.UpdateFile returned %+v, want %+v", updateResponse, want)
}
}
func TestRepositoriesService_DeleteFile(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/contents/p", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
fmt.Fprint(w, `{
"content": null,
"commit":{
"message":"m",
"sha":"f5f369044773ff9c6383c087466d12adb6fa0828"
}
}`)
})
message := "m"
sha := "f5f369044773ff9c6383c087466d12adb6fa0828"
repositoryContentsOptions := &RepositoryContentFileOptions{
Message: &message,
SHA: &sha,
Committer: &CommitAuthor{Name: String("n"), Email: String("e")},
}
deleteResponse, _, err := client.Repositories.DeleteFile("o", "r", "p", repositoryContentsOptions)
if err != nil {
t.Errorf("Repositories.DeleteFile returned error: %v", err)
}
want := &RepositoryContentResponse{
Content: nil,
Commit: Commit{
Message: String("m"),
SHA: String("f5f369044773ff9c6383c087466d12adb6fa0828"),
},
}
if !reflect.DeepEqual(deleteResponse, want) {
t.Errorf("Repositories.DeleteFile returned %+v, want %+v", deleteResponse, want)
}
}
func TestRepositoriesService_GetArchiveLink(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/tarball", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
http.Redirect(w, r, "http://github.com/a", http.StatusFound)
})
url, resp, err := client.Repositories.GetArchiveLink("o", "r", Tarball, &RepositoryContentGetOptions{})
if err != nil {
t.Errorf("Repositories.GetArchiveLink returned error: %v", err)
}
if resp.StatusCode != http.StatusFound {
t.Errorf("Repositories.GetArchiveLink returned status: %d, want %d", resp.StatusCode, http.StatusFound)
}
want := "http://github.com/a"
if url.String() != want {
t.Errorf("Repositories.GetArchiveLink returned %+v, want %+v", url.String(), want)
}
}

Loading…
Cancel
Save