bump deps

Signed-off-by: Alex Suraci <asuraci@pivotal.io>
This commit is contained in:
Chris Brown
2015-02-17 18:08:25 -08:00
committed by Alex Suraci
parent efbfb91683
commit 8b3dc6edbe
57 changed files with 1827 additions and 255 deletions

14
Godeps/Godeps.json generated
View File

@@ -7,8 +7,8 @@
"Deps": [ "Deps": [
{ {
"ImportPath": "code.google.com/p/goauth2/oauth", "ImportPath": "code.google.com/p/goauth2/oauth",
"Comment": "weekly-43", "Comment": "weekly-56",
"Rev": "d4571a95014ff1fdd0055dc0f6b7e977a42b1e5a" "Rev": "afe77d958c701557ec5dc56f6936fcc194d15520"
}, },
{ {
"ImportPath": "github.com/blang/semver", "ImportPath": "github.com/blang/semver",
@@ -17,20 +17,20 @@
}, },
{ {
"ImportPath": "github.com/google/go-github/github", "ImportPath": "github.com/google/go-github/github",
"Rev": "32eadd61120c7a84b8eeac3ac3795cb453a33054" "Rev": "7ea4ee6d222607c11ea86e99a6f6723beeae785d"
}, },
{ {
"ImportPath": "github.com/google/go-querystring/query", "ImportPath": "github.com/google/go-querystring/query",
"Rev": "ec0a78e0f4db229b7897be36596a8944230b857a" "Rev": "d8840cbb2baa915f4836edda4750050a2c0b7aea"
}, },
{ {
"ImportPath": "github.com/mitchellh/colorstring", "ImportPath": "github.com/mitchellh/colorstring",
"Rev": "9797cb57e314a5a2c4d735ff060d5c6dcba57749" "Rev": "15fc698eaae194ff160846daf77922a45b9290a7"
}, },
{ {
"ImportPath": "github.com/onsi/ginkgo", "ImportPath": "github.com/onsi/ginkgo",
"Comment": "v1.1.0-33-g17ea479", "Comment": "v1.1.0-38-g5ed93e4",
"Rev": "17ea479729ee427265ac1e913443018350946ddf" "Rev": "5ed93e443a4b7dfe9f5e95ca87e6082e503021d2"
}, },
{ {
"ImportPath": "github.com/onsi/gomega", "ImportPath": "github.com/onsi/gomega",

View File

@@ -67,7 +67,7 @@ func main() {
// Get an authorization code from the data provider. // Get an authorization code from the data provider.
// ("Please ask the user if I can access this resource.") // ("Please ask the user if I can access this resource.")
url := config.AuthCodeURL("") url := config.AuthCodeURL("")
fmt.Println("Visit this URL to get a code, then run again with -code=YOUR_CODE\n") fmt.Print("Visit this URL to get a code, then run again with -code=YOUR_CODE\n\n")
fmt.Println(url) fmt.Println(url)
return return
} }

View File

@@ -2,8 +2,7 @@
// Use of this source code is governed by a BSD-style // Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file. // license that can be found in the LICENSE file.
// The oauth package provides support for making // Package oauth supports making OAuth2-authenticated HTTP requests.
// OAuth2-authenticated HTTP requests.
// //
// Example usage: // Example usage:
// //
@@ -39,14 +38,23 @@ package oauth
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt"
"io"
"io/ioutil" "io/ioutil"
"mime" "mime"
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"strconv"
"strings"
"sync"
"time" "time"
) )
// OAuthError is the error type returned by many operations.
//
// In retrospect it should not exist. Don't depend on it.
type OAuthError struct { type OAuthError struct {
prefix string prefix string
msg string msg string
@@ -122,7 +130,16 @@ type Config struct {
// TokenCache allows tokens to be cached for subsequent requests. // TokenCache allows tokens to be cached for subsequent requests.
TokenCache Cache TokenCache Cache
AccessType string // Optional, "online" (default) or "offline", no refresh token if "online" // AccessType is an OAuth extension that gets sent as the
// "access_type" field in the URL from AuthCodeURL.
// See https://developers.google.com/accounts/docs/OAuth2WebServer.
// It may be "online" (the default) or "offline".
// If your application needs to refresh access tokens when the
// user is not present at the browser, then use offline. This
// will result in your application obtaining a refresh token
// the first time your application exchanges an authorization
// code for a user.
AccessType string
// ApprovalPrompt indicates whether the user should be // ApprovalPrompt indicates whether the user should be
// re-prompted for consent. If set to "auto" (default) the // re-prompted for consent. If set to "auto" (default) the
@@ -139,11 +156,20 @@ type Config struct {
type Token struct { type Token struct {
AccessToken string AccessToken string
RefreshToken string RefreshToken string
Expiry time.Time // If zero the token has no (known) expiry time. Expiry time.Time // If zero the token has no (known) expiry time.
Extra map[string]string // May be nil.
// Extra optionally contains extra metadata from the server
// when updating a token. The only current key that may be
// populated is "id_token". It may be nil and will be
// initialized as needed.
Extra map[string]string
} }
// Expired reports whether the token has expired or is invalid.
func (t *Token) Expired() bool { func (t *Token) Expired() bool {
if t.AccessToken == "" {
return true
}
if t.Expiry.IsZero() { if t.Expiry.IsZero() {
return false return false
} }
@@ -164,6 +190,9 @@ type Transport struct {
*Config *Config
*Token *Token
// mu guards modifying the token.
mu sync.Mutex
// Transport is the HTTP transport to use when making requests. // Transport is the HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil. // It will default to http.DefaultTransport if nil.
// (It should never be an oauth.Transport.) // (It should never be an oauth.Transport.)
@@ -192,11 +221,11 @@ func (c *Config) AuthCodeURL(state string) string {
q := url.Values{ q := url.Values{
"response_type": {"code"}, "response_type": {"code"},
"client_id": {c.ClientId}, "client_id": {c.ClientId},
"redirect_uri": {c.RedirectURL}, "state": condVal(state),
"scope": {c.Scope}, "scope": condVal(c.Scope),
"state": {state}, "redirect_uri": condVal(c.RedirectURL),
"access_type": {c.AccessType}, "access_type": condVal(c.AccessType),
"approval_prompt": {c.ApprovalPrompt}, "approval_prompt": condVal(c.ApprovalPrompt),
}.Encode() }.Encode()
if url_.RawQuery == "" { if url_.RawQuery == "" {
url_.RawQuery = q url_.RawQuery = q
@@ -206,6 +235,13 @@ func (c *Config) AuthCodeURL(state string) string {
return url_.String() return url_.String()
} }
func condVal(v string) []string {
if v == "" {
return nil
}
return []string{v}
}
// Exchange takes a code and gets access Token from the remote server. // Exchange takes a code and gets access Token from the remote server.
func (t *Transport) Exchange(code string) (*Token, error) { func (t *Transport) Exchange(code string) (*Token, error) {
if t.Config == nil { if t.Config == nil {
@@ -246,35 +282,48 @@ func (t *Transport) Exchange(code string) (*Token, error) {
// If the Token is invalid callers should expect HTTP-level errors, // If the Token is invalid callers should expect HTTP-level errors,
// as indicated by the Response's StatusCode. // as indicated by the Response's StatusCode.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
accessToken, err := t.getAccessToken()
if err != nil {
return nil, err
}
// To set the Authorization header, we must make a copy of the Request
// so that we don't modify the Request we were given.
// This is required by the specification of http.RoundTripper.
req = cloneRequest(req)
req.Header.Set("Authorization", "Bearer "+accessToken)
// Make the HTTP request.
return t.transport().RoundTrip(req)
}
func (t *Transport) getAccessToken() (string, error) {
t.mu.Lock()
defer t.mu.Unlock()
if t.Token == nil { if t.Token == nil {
if t.Config == nil { if t.Config == nil {
return nil, OAuthError{"RoundTrip", "no Config supplied"} return "", OAuthError{"RoundTrip", "no Config supplied"}
} }
if t.TokenCache == nil { if t.TokenCache == nil {
return nil, OAuthError{"RoundTrip", "no Token supplied"} return "", OAuthError{"RoundTrip", "no Token supplied"}
} }
var err error var err error
t.Token, err = t.TokenCache.Token() t.Token, err = t.TokenCache.Token()
if err != nil { if err != nil {
return nil, err return "", err
} }
} }
// Refresh the Token if it has expired. // Refresh the Token if it has expired.
if t.Expired() { if t.Expired() {
if err := t.Refresh(); err != nil { if err := t.Refresh(); err != nil {
return nil, err return "", err
} }
} }
if t.AccessToken == "" {
// To set the Authorization header, we must make a copy of the Request return "", errors.New("no access token obtained from refresh")
// so that we don't modify the Request we were given. }
// This is required by the specification of http.RoundTripper. return t.AccessToken, nil
req = cloneRequest(req)
req.Header.Set("Authorization", "Bearer "+t.AccessToken)
// Make the HTTP request.
return t.transport().RoundTrip(req)
} }
// cloneRequest returns a clone of the provided *http.Request. // cloneRequest returns a clone of the provided *http.Request.
@@ -316,31 +365,80 @@ func (t *Transport) Refresh() error {
return nil return nil
} }
// AuthenticateClient gets an access Token using the client_credentials grant
// type.
func (t *Transport) AuthenticateClient() error {
if t.Config == nil {
return OAuthError{"Exchange", "no Config supplied"}
}
if t.Token == nil {
t.Token = &Token{}
}
return t.updateToken(t.Token, url.Values{"grant_type": {"client_credentials"}})
}
// providerAuthHeaderWorks reports whether the OAuth2 server identified by the tokenURL
// implements the OAuth2 spec correctly
// See https://code.google.com/p/goauth2/issues/detail?id=31 for background.
// In summary:
// - Reddit only accepts client secret in the Authorization header
// - Dropbox accepts either it in URL param or Auth header, but not both.
// - Google only accepts URL param (not spec compliant?), not Auth header
func providerAuthHeaderWorks(tokenURL string) bool {
if strings.HasPrefix(tokenURL, "https://accounts.google.com/") ||
strings.HasPrefix(tokenURL, "https://github.com/") ||
strings.HasPrefix(tokenURL, "https://api.instagram.com/") ||
strings.HasPrefix(tokenURL, "https://www.douban.com/") {
// Some sites fail to implement the OAuth2 spec fully.
return false
}
// Assume the provider implements the spec properly
// otherwise. We can add more exceptions as they're
// discovered. We will _not_ be adding configurable hooks
// to this package to let users select server bugs.
return true
}
// updateToken mutates both tok and v.
func (t *Transport) updateToken(tok *Token, v url.Values) error { func (t *Transport) updateToken(tok *Token, v url.Values) error {
v.Set("client_id", t.ClientId) v.Set("client_id", t.ClientId)
v.Set("client_secret", t.ClientSecret) bustedAuth := !providerAuthHeaderWorks(t.TokenURL)
r, err := (&http.Client{Transport: t.transport()}).PostForm(t.TokenURL, v) if bustedAuth {
v.Set("client_secret", t.ClientSecret)
}
client := &http.Client{Transport: t.transport()}
req, err := http.NewRequest("POST", t.TokenURL, strings.NewReader(v.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if !bustedAuth {
req.SetBasicAuth(t.ClientId, t.ClientSecret)
}
r, err := client.Do(req)
if err != nil { if err != nil {
return err return err
} }
defer r.Body.Close() defer r.Body.Close()
if r.StatusCode != 200 { if r.StatusCode != 200 {
return OAuthError{"updateToken", r.Status} return OAuthError{"updateToken", "Unexpected HTTP status " + r.Status}
} }
var b struct { var b struct {
Access string `json:"access_token"` Access string `json:"access_token"`
Refresh string `json:"refresh_token"` Refresh string `json:"refresh_token"`
ExpiresIn time.Duration `json:"expires_in"` ExpiresIn int64 `json:"expires_in"` // seconds
Id string `json:"id_token"` Id string `json:"id_token"`
}
body, err := ioutil.ReadAll(io.LimitReader(r.Body, 1<<20))
if err != nil {
return err
} }
content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type")) content, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
switch content { switch content {
case "application/x-www-form-urlencoded", "text/plain": case "application/x-www-form-urlencoded", "text/plain":
body, err := ioutil.ReadAll(r.Body)
if err != nil {
return err
}
vals, err := url.ParseQuery(string(body)) vals, err := url.ParseQuery(string(body))
if err != nil { if err != nil {
return err return err
@@ -348,25 +446,25 @@ func (t *Transport) updateToken(tok *Token, v url.Values) error {
b.Access = vals.Get("access_token") b.Access = vals.Get("access_token")
b.Refresh = vals.Get("refresh_token") b.Refresh = vals.Get("refresh_token")
b.ExpiresIn, _ = time.ParseDuration(vals.Get("expires_in") + "s") b.ExpiresIn, _ = strconv.ParseInt(vals.Get("expires_in"), 10, 64)
b.Id = vals.Get("id_token") b.Id = vals.Get("id_token")
default: default:
if err = json.NewDecoder(r.Body).Decode(&b); err != nil { if err = json.Unmarshal(body, &b); err != nil {
return err return fmt.Errorf("got bad response from server: %q", body)
} }
// The JSON parser treats the unitless ExpiresIn like 'ns' instead of 's' as above, }
// so compensate here. if b.Access == "" {
b.ExpiresIn *= time.Second return errors.New("received empty access token from authorization server")
} }
tok.AccessToken = b.Access tok.AccessToken = b.Access
// Don't overwrite `RefreshToken` with an empty value // Don't overwrite `RefreshToken` with an empty value
if len(b.Refresh) > 0 { if b.Refresh != "" {
tok.RefreshToken = b.Refresh tok.RefreshToken = b.Refresh
} }
if b.ExpiresIn == 0 { if b.ExpiresIn == 0 {
tok.Expiry = time.Time{} tok.Expiry = time.Time{}
} else { } else {
tok.Expiry = time.Now().Add(b.ExpiresIn) tok.Expiry = time.Now().Add(time.Duration(b.ExpiresIn) * time.Second)
} }
if b.Id != "" { if b.Id != "" {
if tok.Extra == nil { if tok.Extra == nil {

View File

@@ -25,6 +25,7 @@ var requests = []struct {
path: "/token", path: "/token",
query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d", query: "grant_type=authorization_code&code=c0d3&client_id=cl13nt1d",
contenttype: "application/json", contenttype: "application/json",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
body: ` body: `
{ {
"access_token":"token1", "access_token":"token1",
@@ -39,6 +40,7 @@ var requests = []struct {
path: "/token", path: "/token",
query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d", query: "grant_type=refresh_token&refresh_token=refreshtoken1&client_id=cl13nt1d",
contenttype: "application/json", contenttype: "application/json",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
body: ` body: `
{ {
"access_token":"token2", "access_token":"token2",
@@ -54,8 +56,22 @@ var requests = []struct {
query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d", query: "grant_type=refresh_token&refresh_token=refreshtoken2&client_id=cl13nt1d",
contenttype: "application/x-www-form-urlencoded", contenttype: "application/x-www-form-urlencoded",
body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600", body: "access_token=token3&refresh_token=refreshtoken3&id_token=idtoken3&expires_in=3600",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
}, },
{path: "/secure", auth: "Bearer token3", body: "third payload"}, {path: "/secure", auth: "Bearer token3", body: "third payload"},
{
path: "/token",
query: "grant_type=client_credentials&client_id=cl13nt1d",
contenttype: "application/json",
auth: "Basic Y2wxM250MWQ6czNjcjN0",
body: `
{
"access_token":"token4",
"expires_in":3600
}
`,
},
{path: "/secure", auth: "Bearer token4", body: "fourth payload"},
} }
func TestOAuth(t *testing.T) { func TestOAuth(t *testing.T) {
@@ -131,6 +147,18 @@ func TestOAuth(t *testing.T) {
} }
checkBody(t, resp, "third payload") checkBody(t, resp, "third payload")
checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3") checkToken(t, transport.Token, "token3", "refreshtoken3", "idtoken3")
transport.Token = &Token{}
err = transport.AuthenticateClient()
if err != nil {
t.Fatalf("AuthenticateClient: %v", err)
}
checkToken(t, transport.Token, "token4", "", "")
resp, err = c.Get(server.URL + "/secure")
if err != nil {
t.Fatalf("Get: %v", err)
}
checkBody(t, resp, "fourth payload")
} }
func checkToken(t *testing.T, tok *Token, access, refresh, id string) { func checkToken(t *testing.T, tok *Token, access, refresh, id string) {
@@ -143,16 +171,21 @@ func checkToken(t *testing.T, tok *Token, access, refresh, id string) {
if g, w := tok.Extra["id_token"], id; g != w { if g, w := tok.Extra["id_token"], id; g != w {
t.Errorf("Extra['id_token'] = %q, want %q", g, w) t.Errorf("Extra['id_token'] = %q, want %q", g, w)
} }
exp := tok.Expiry.Sub(time.Now()) if tok.Expiry.IsZero() {
if (time.Hour-time.Second) > exp || exp > time.Hour { t.Errorf("Expiry is zero; want ~1 hour")
t.Errorf("Expiry = %v, want ~1 hour", exp) } else {
exp := tok.Expiry.Sub(time.Now())
const slop = 3 * time.Second // time moving during test
if (time.Hour-slop) > exp || exp > time.Hour {
t.Errorf("Expiry = %v, want ~1 hour", exp)
}
} }
} }
func checkBody(t *testing.T, r *http.Response, body string) { func checkBody(t *testing.T, r *http.Response, body string) {
b, err := ioutil.ReadAll(r.Body) b, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
t.Error("reading reponse body: %v, want %q", err, body) t.Errorf("reading reponse body: %v, want %q", err, body)
} }
if g, w := string(b), body; g != w { if g, w := string(b), body; g != w {
t.Errorf("request body mismatch: got %q, want %q", g, w) t.Errorf("request body mismatch: got %q, want %q", g, w)
@@ -184,3 +217,20 @@ func TestCachePermissions(t *testing.T) {
t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode()) t.Errorf("Created cache file has mode %#o, want non-accessible to group+other", fi.Mode())
} }
} }
func TestTokenExpired(t *testing.T) {
tests := []struct {
token Token
expired bool
}{
{Token{AccessToken: "foo"}, false},
{Token{AccessToken: ""}, true},
{Token{AccessToken: "foo", Expiry: time.Now().Add(-1 * time.Hour)}, true},
{Token{AccessToken: "foo", Expiry: time.Now().Add(1 * time.Hour)}, false},
}
for _, tt := range tests {
if got := tt.token.Expired(); got != tt.expired {
t.Errorf("token %+v Expired = %v; want %v", tt.token, got, !got)
}
}
}

View File

@@ -47,8 +47,9 @@ type PushEvent struct {
PushID *int `json:"push_id,omitempty"` PushID *int `json:"push_id,omitempty"`
Head *string `json:"head,omitempty"` Head *string `json:"head,omitempty"`
Ref *string `json:"ref,omitempty"` Ref *string `json:"ref,omitempty"`
Size *int `json:"ref,omitempty"` Size *int `json:"size,omitempty"`
Commits []PushEventCommit `json:"commits,omitempty"` Commits []PushEventCommit `json:"commits,omitempty"`
Repo *Repository `json:"repository,omitempty"`
} }
func (p PushEvent) String() string { func (p PushEvent) String() string {
@@ -61,13 +62,44 @@ type PushEventCommit struct {
Message *string `json:"message,omitempty"` Message *string `json:"message,omitempty"`
Author *CommitAuthor `json:"author,omitempty"` Author *CommitAuthor `json:"author,omitempty"`
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
Distinct *bool `json:"distinct"` Distinct *bool `json:"distinct,omitempty"`
Added []string `json:"added,omitempty"`
Removed []string `json:"removed,omitempty"`
Modified []string `json:"modified,omitempty"`
} }
func (p PushEventCommit) String() string { func (p PushEventCommit) String() string {
return Stringify(p) return Stringify(p)
} }
//PullRequestEvent represents the payload delivered by PullRequestEvent webhook
type PullRequestEvent struct {
Action *string `json:"action,omitempty"`
Number *int `json:"number,omitempty"`
PullRequest *PullRequest `json:"pull_request,omitempty"`
Repo *Repository `json:"repository,omitempty"`
Sender *User `json:"sender,omitempty"`
}
// IssueActivityEvent represents the payload delivered by Issue webhook
type IssueActivityEvent struct {
Action *string `json:"action,omitempty"`
Issue *Issue `json:"issue,omitempty"`
Repo *Repository `json:"repository,omitempty"`
Sender *User `json:"sender,omitempty"`
}
// IssueCommentEvent represents the payload delivered by IssueComment webhook
//
// This webhook also gets fired for comments on pull requests
type IssueCommentEvent struct {
Action *string `json:"action,omitempty"`
Issue *Issue `json:"issue,omitempty"`
Comment *IssueComment `json:"comment,omitempty"`
Repo *Repository `json:"repository,omitempty"`
Sender *User `json:"sender,omitempty"`
}
// ListEvents drinks from the firehose of all public events across GitHub. // ListEvents drinks from the firehose of all public events across GitHub.
// //
// GitHub API docs: http://developer.github.com/v3/activity/events/#list-public-events // GitHub API docs: http://developer.github.com/v3/activity/events/#list-public-events

View File

@@ -95,5 +95,31 @@ bool, and int values. For example:
client.Repositories.Create("", repo) client.Repositories.Create("", repo)
Users who have worked with protocol buffers should find this pattern familiar. Users who have worked with protocol buffers should find this pattern familiar.
Pagination
All requests for resource collections (repos, pull requests, issues, etc)
support pagination. Pagination options are described in the
ListOptions struct and passed to the list methods directly or as an
embedded type of a more specific list options struct (for example
PullRequestListOptions). Pages information is available via Response struct.
opt := &github.RepositoryListByOrgOptions{
ListOptions: github.ListOptions{PerPage: 10},
}
// get all pages of results
var allRepos []github.Repository
for {
repos, resp, err := client.Repositories.ListByOrg("github", opt)
if err != nil {
return err
}
allRepos = append(allRepos, repos...)
if resp.NextPage == 0 {
break
}
opt.ListOptions.Page = resp.NextPage
}
*/ */
package github package github

View File

@@ -30,6 +30,7 @@ type Gist struct {
GitPullURL *string `json:"git_pull_url,omitempty"` GitPullURL *string `json:"git_pull_url,omitempty"`
GitPushURL *string `json:"git_push_url,omitempty"` GitPushURL *string `json:"git_push_url,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }
func (g Gist) String() string { func (g Gist) String() string {

View File

@@ -19,7 +19,12 @@ type Commit struct {
Tree *Tree `json:"tree,omitempty"` Tree *Tree `json:"tree,omitempty"`
Parents []Commit `json:"parents,omitempty"` Parents []Commit `json:"parents,omitempty"`
Stats *CommitStats `json:"stats,omitempty"` Stats *CommitStats `json:"stats,omitempty"`
URL *string `json:url,omitempty"` URL *string `json:"url,omitempty"`
// CommentCount is the number of GitHub comments on the commit. This
// is only populated for requests that fetch GitHub data like
// Pulls.ListCommits, Repositories.ListCommits, etc.
CommentCount *int `json:"comment_count,omitempty"`
} }
func (c Commit) String() string { func (c Commit) String() string {
@@ -57,6 +62,15 @@ func (s *GitService) GetCommit(owner string, repo string, sha string) (*Commit,
return c, resp, err return c, resp, err
} }
// createCommit represents the body of a CreateCommit request.
type createCommit struct {
Author *CommitAuthor `json:"author,omitempty"`
Committer *CommitAuthor `json:"committer,omitempty"`
Message *string `json:"message,omitempty"`
Tree *string `json:"tree,omitempty"`
Parents []string `json:"parents,omitempty"`
}
// CreateCommit creates a new commit in a repository. // CreateCommit creates a new commit in a repository.
// //
// The commit.Committer is optional and will be filled with the commit.Author // The commit.Committer is optional and will be filled with the commit.Author
@@ -66,7 +80,24 @@ func (s *GitService) GetCommit(owner string, repo string, sha string) (*Commit,
// GitHub API docs: http://developer.github.com/v3/git/commits/#create-a-commit // GitHub API docs: http://developer.github.com/v3/git/commits/#create-a-commit
func (s *GitService) CreateCommit(owner string, repo string, commit *Commit) (*Commit, *Response, error) { func (s *GitService) CreateCommit(owner string, repo string, commit *Commit) (*Commit, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/git/commits", owner, repo) u := fmt.Sprintf("repos/%v/%v/git/commits", owner, repo)
req, err := s.client.NewRequest("POST", u, commit)
body := &createCommit{}
if commit != nil {
parents := make([]string, len(commit.Parents))
for i, parent := range commit.Parents {
parents[i] = *parent.SHA
}
body = &createCommit{
Author: commit.Author,
Committer: commit.Committer,
Message: commit.Message,
Tree: commit.Tree.SHA,
Parents: parents,
}
}
req, err := s.client.NewRequest("POST", u, body)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@@ -42,15 +42,25 @@ func TestGitService_CreateCommit(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
input := &Commit{Message: String("m"), Tree: &Tree{SHA: String("t")}} input := &Commit{
Message: String("m"),
Tree: &Tree{SHA: String("t")},
Parents: []Commit{{SHA: String("p")}},
}
mux.HandleFunc("/repos/o/r/git/commits", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/repos/o/r/git/commits", func(w http.ResponseWriter, r *http.Request) {
v := new(Commit) v := new(createCommit)
json.NewDecoder(r.Body).Decode(v) json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST") testMethod(t, r, "POST")
if !reflect.DeepEqual(v, input) {
t.Errorf("Request body = %+v, want %+v", v, input) want := &createCommit{
Message: input.Message,
Tree: String("t"),
Parents: []string{"p"},
}
if !reflect.DeepEqual(v, want) {
t.Errorf("Request body = %+v, want %+v", v, want)
} }
fmt.Fprint(w, `{"sha":"s"}`) fmt.Fprint(w, `{"sha":"s"}`)
}) })

View File

@@ -7,6 +7,7 @@ package github
import ( import (
"fmt" "fmt"
"strings"
) )
// Reference represents a GitHub reference. // Reference represents a GitHub reference.
@@ -47,6 +48,7 @@ type updateRefRequest struct {
// //
// GitHub API docs: http://developer.github.com/v3/git/refs/#get-a-reference // GitHub API docs: http://developer.github.com/v3/git/refs/#get-a-reference
func (s *GitService) GetRef(owner string, repo string, ref string) (*Reference, *Response, error) { func (s *GitService) GetRef(owner string, repo string, ref string) (*Reference, *Response, error) {
ref = strings.TrimPrefix(ref, "refs/")
u := fmt.Sprintf("repos/%v/%v/git/refs/%v", owner, repo, ref) u := fmt.Sprintf("repos/%v/%v/git/refs/%v", owner, repo, ref)
req, err := s.client.NewRequest("GET", u, nil) req, err := s.client.NewRequest("GET", u, nil)
if err != nil { if err != nil {
@@ -105,7 +107,8 @@ func (s *GitService) ListRefs(owner, repo string, opt *ReferenceListOptions) ([]
func (s *GitService) CreateRef(owner string, repo string, ref *Reference) (*Reference, *Response, error) { func (s *GitService) CreateRef(owner string, repo string, ref *Reference) (*Reference, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/git/refs", owner, repo) u := fmt.Sprintf("repos/%v/%v/git/refs", owner, repo)
req, err := s.client.NewRequest("POST", u, &createRefRequest{ req, err := s.client.NewRequest("POST", u, &createRefRequest{
Ref: String("refs/" + *ref.Ref), // back-compat with previous behavior that didn't require 'refs/' prefix
Ref: String("refs/" + strings.TrimPrefix(*ref.Ref, "refs/")),
SHA: ref.Object.SHA, SHA: ref.Object.SHA,
}) })
if err != nil { if err != nil {
@@ -125,7 +128,8 @@ func (s *GitService) CreateRef(owner string, repo string, ref *Reference) (*Refe
// //
// GitHub API docs: http://developer.github.com/v3/git/refs/#update-a-reference // GitHub API docs: http://developer.github.com/v3/git/refs/#update-a-reference
func (s *GitService) UpdateRef(owner string, repo string, ref *Reference, force bool) (*Reference, *Response, error) { func (s *GitService) UpdateRef(owner string, repo string, ref *Reference, force bool) (*Reference, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/git/refs/%v", owner, repo, *ref.Ref) refPath := strings.TrimPrefix(*ref.Ref, "refs/")
u := fmt.Sprintf("repos/%v/%v/git/refs/%v", owner, repo, refPath)
req, err := s.client.NewRequest("PATCH", u, &updateRefRequest{ req, err := s.client.NewRequest("PATCH", u, &updateRefRequest{
SHA: ref.Object.SHA, SHA: ref.Object.SHA,
Force: &force, Force: &force,
@@ -147,6 +151,7 @@ func (s *GitService) UpdateRef(owner string, repo string, ref *Reference, force
// //
// GitHub API docs: http://developer.github.com/v3/git/refs/#delete-a-reference // GitHub API docs: http://developer.github.com/v3/git/refs/#delete-a-reference
func (s *GitService) DeleteRef(owner string, repo string, ref string) (*Response, error) { func (s *GitService) DeleteRef(owner string, repo string, ref string) (*Response, error) {
ref = strings.TrimPrefix(ref, "refs/")
u := fmt.Sprintf("repos/%v/%v/git/refs/%v", owner, repo, ref) u := fmt.Sprintf("repos/%v/%v/git/refs/%v", owner, repo, ref)
req, err := s.client.NewRequest("DELETE", u, nil) req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil { if err != nil {

View File

@@ -31,7 +31,7 @@ func TestGitService_GetRef(t *testing.T) {
}`) }`)
}) })
ref, _, err := client.Git.GetRef("o", "r", "heads/b") ref, _, err := client.Git.GetRef("o", "r", "refs/heads/b")
if err != nil { if err != nil {
t.Errorf("Git.GetRef returned error: %v", err) t.Errorf("Git.GetRef returned error: %v", err)
} }
@@ -48,6 +48,11 @@ func TestGitService_GetRef(t *testing.T) {
if !reflect.DeepEqual(ref, want) { if !reflect.DeepEqual(ref, want) {
t.Errorf("Git.GetRef returned %+v, want %+v", ref, want) t.Errorf("Git.GetRef returned %+v, want %+v", ref, want)
} }
// without 'refs/' prefix
if _, _, err := client.Git.GetRef("o", "r", "heads/b"); err != nil {
t.Errorf("Git.GetRef returned error: %v", err)
}
} }
func TestGitService_ListRefs(t *testing.T) { func TestGitService_ListRefs(t *testing.T) {
@@ -161,7 +166,7 @@ func TestGitService_CreateRef(t *testing.T) {
}) })
ref, _, err := client.Git.CreateRef("o", "r", &Reference{ ref, _, err := client.Git.CreateRef("o", "r", &Reference{
Ref: String("heads/b"), Ref: String("refs/heads/b"),
Object: &GitObject{ Object: &GitObject{
SHA: String("aa218f56b14c9653891f9e74264a383fa43fefbd"), SHA: String("aa218f56b14c9653891f9e74264a383fa43fefbd"),
}, },
@@ -182,6 +187,17 @@ func TestGitService_CreateRef(t *testing.T) {
if !reflect.DeepEqual(ref, want) { if !reflect.DeepEqual(ref, want) {
t.Errorf("Git.CreateRef returned %+v, want %+v", ref, want) t.Errorf("Git.CreateRef returned %+v, want %+v", ref, want)
} }
// without 'refs/' prefix
_, _, err = client.Git.CreateRef("o", "r", &Reference{
Ref: String("heads/b"),
Object: &GitObject{
SHA: String("aa218f56b14c9653891f9e74264a383fa43fefbd"),
},
})
if err != nil {
t.Errorf("Git.CreateRef returned error: %v", err)
}
} }
func TestGitService_UpdateRef(t *testing.T) { func TestGitService_UpdateRef(t *testing.T) {
@@ -214,7 +230,7 @@ func TestGitService_UpdateRef(t *testing.T) {
}) })
ref, _, err := client.Git.UpdateRef("o", "r", &Reference{ ref, _, err := client.Git.UpdateRef("o", "r", &Reference{
Ref: String("heads/b"), Ref: String("refs/heads/b"),
Object: &GitObject{SHA: String("aa218f56b14c9653891f9e74264a383fa43fefbd")}, Object: &GitObject{SHA: String("aa218f56b14c9653891f9e74264a383fa43fefbd")},
}, true) }, true)
if err != nil { if err != nil {
@@ -233,6 +249,15 @@ func TestGitService_UpdateRef(t *testing.T) {
if !reflect.DeepEqual(ref, want) { if !reflect.DeepEqual(ref, want) {
t.Errorf("Git.UpdateRef returned %+v, want %+v", ref, want) t.Errorf("Git.UpdateRef returned %+v, want %+v", ref, want)
} }
// without 'refs/' prefix
_, _, err = client.Git.UpdateRef("o", "r", &Reference{
Ref: String("heads/b"),
Object: &GitObject{SHA: String("aa218f56b14c9653891f9e74264a383fa43fefbd")},
}, true)
if err != nil {
t.Errorf("Git.UpdateRef returned error: %v", err)
}
} }
func TestGitService_DeleteRef(t *testing.T) { func TestGitService_DeleteRef(t *testing.T) {
@@ -243,8 +268,13 @@ func TestGitService_DeleteRef(t *testing.T) {
testMethod(t, r, "DELETE") testMethod(t, r, "DELETE")
}) })
_, err := client.Git.DeleteRef("o", "r", "heads/b") _, err := client.Git.DeleteRef("o", "r", "refs/heads/b")
if err != nil { if err != nil {
t.Errorf("Git.DeleteRef returned error: %v", err) t.Errorf("Git.DeleteRef returned error: %v", err)
} }
// without 'refs/' prefix
if _, err := client.Git.DeleteRef("o", "r", "heads/b"); err != nil {
t.Errorf("Git.DeleteRef returned error: %v", err)
}
} }

View File

@@ -21,11 +21,12 @@ func (t Tree) String() string {
// represent either a blob, a commit (in the case of a submodule), or another // represent either a blob, a commit (in the case of a submodule), or another
// tree. // tree.
type TreeEntry struct { type TreeEntry struct {
SHA *string `json:"sha,omitempty"` SHA *string `json:"sha,omitempty"`
Path *string `json:"path,omitempty"` Path *string `json:"path,omitempty"`
Mode *string `json:"mode,omitempty"` Mode *string `json:"mode,omitempty"`
Type *string `json:"type,omitempty"` Type *string `json:"type,omitempty"`
Size *int `json:"size,omitempty"` Size *int `json:"size,omitempty"`
Content *string `json:"content,omitempty"`
} }
func (t TreeEntry) String() string { func (t TreeEntry) String() string {
@@ -57,7 +58,7 @@ func (s *GitService) GetTree(owner string, repo string, sha string, recursive bo
// createTree represents the body of a CreateTree request. // createTree represents the body of a CreateTree request.
type createTree struct { type createTree struct {
BaseTree string `json:"base_tree"` BaseTree string `json:"base_tree,omitempty"`
Entries []TreeEntry `json:"tree"` Entries []TreeEntry `json:"tree"`
} }

View File

@@ -80,7 +80,7 @@ func TestGitService_CreateTree(t *testing.T) {
} }
fmt.Fprint(w, `{ fmt.Fprint(w, `{
"sha": "cd8274d15fa3ae2ab983129fb037999f264ba9a7", "sha": "cd8274d15fa3ae2ab983129fb037999f264ba9a7",
"tree": [ "tree": [
{ {
"path": "file.rb", "path": "file.rb",
@@ -116,6 +116,73 @@ func TestGitService_CreateTree(t *testing.T) {
} }
} }
func TestGitService_CreateTree_Content(t *testing.T) {
setup()
defer teardown()
input := []TreeEntry{
{
Path: String("content.md"),
Mode: String("100644"),
Content: String("file content"),
},
}
mux.HandleFunc("/repos/o/r/git/trees", func(w http.ResponseWriter, r *http.Request) {
v := new(createTree)
json.NewDecoder(r.Body).Decode(v)
if m := "POST"; m != r.Method {
t.Errorf("Request method = %v, want %v", r.Method, m)
}
want := &createTree{
BaseTree: "b",
Entries: input,
}
if !reflect.DeepEqual(v, want) {
t.Errorf("Git.CreateTree request body: %+v, want %+v", v, want)
}
fmt.Fprint(w, `{
"sha": "5c6780ad2c68743383b740fd1dab6f6a33202b11",
"url": "https://api.github.com/repos/o/r/git/trees/5c6780ad2c68743383b740fd1dab6f6a33202b11",
"tree": [
{
"mode": "100644",
"type": "blob",
"sha": "aad8feacf6f8063150476a7b2bd9770f2794c08b",
"path": "content.md",
"size": 12,
"url": "https://api.github.com/repos/o/r/git/blobs/aad8feacf6f8063150476a7b2bd9770f2794c08b"
}
]
}`)
})
tree, _, err := client.Git.CreateTree("o", "r", "b", input)
if err != nil {
t.Errorf("Git.CreateTree returned error: %v", err)
}
want := Tree{
String("5c6780ad2c68743383b740fd1dab6f6a33202b11"),
[]TreeEntry{
{
Path: String("content.md"),
Mode: String("100644"),
Type: String("blob"),
Size: Int(12),
SHA: String("aad8feacf6f8063150476a7b2bd9770f2794c08b"),
},
},
}
if !reflect.DeepEqual(*tree, want) {
t.Errorf("Git.CreateTree returned %+v, want %+v", *tree, want)
}
}
func TestGitService_CreateTree_invalidOwner(t *testing.T) { func TestGitService_CreateTree_invalidOwner(t *testing.T) {
_, _, err := client.Git.CreateTree("%", "%", "", nil) _, _, err := client.Git.CreateTree("%", "%", "", nil)
testURLParseError(t, err) testURLParseError(t, err)

View File

@@ -34,6 +34,14 @@ const (
mediaTypeV3 = "application/vnd.github.v3+json" mediaTypeV3 = "application/vnd.github.v3+json"
defaultMediaType = "application/octet-stream" defaultMediaType = "application/octet-stream"
// Media Type values to access preview APIs
// https://developer.github.com/changes/2014-08-05-team-memberships-api/
mediaTypeMembershipPreview = "application/vnd.github.the-wasp-preview+json"
// https://developer.github.com/changes/2014-01-09-preview-the-new-deployments-api/
mediaTypeDeploymentPreview = "application/vnd.github.cannonball-preview+json"
) )
// A Client manages communication with the GitHub API. // A Client manages communication with the GitHub API.
@@ -146,8 +154,9 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
u := c.BaseURL.ResolveReference(rel) u := c.BaseURL.ResolveReference(rel)
buf := new(bytes.Buffer) var buf io.ReadWriter
if body != nil { if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body) err := json.NewEncoder(buf).Encode(body)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -160,7 +169,9 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
} }
req.Header.Add("Accept", mediaTypeV3) req.Header.Add("Accept", mediaTypeV3)
req.Header.Add("User-Agent", c.UserAgent) if c.UserAgent != "" {
req.Header.Add("User-Agent", c.UserAgent)
}
return req, nil return req, nil
} }
@@ -485,10 +496,10 @@ type UnauthenticatedRateLimitedTransport struct {
// RoundTrip implements the RoundTripper interface. // RoundTrip implements the RoundTripper interface.
func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
if t.ClientID == "" { if t.ClientID == "" {
return nil, errors.New("ClientID is empty") return nil, errors.New("t.ClientID is empty")
} }
if t.ClientSecret == "" { if t.ClientSecret == "" {
return nil, errors.New("ClientSecret is empty") return nil, errors.New("t.ClientSecret is empty")
} }
// To set extra querystring params, we must make a copy of the Request so // To set extra querystring params, we must make a copy of the Request so

View File

@@ -80,8 +80,8 @@ func openTestFile(name, content string) (file *os.File, dir string, err error) {
} }
func testMethod(t *testing.T, r *http.Request, want string) { func testMethod(t *testing.T, r *http.Request, want string) {
if want != r.Method { if got := r.Method; got != want {
t.Errorf("Request method = %v, want %v", r.Method, want) t.Errorf("Request method: %v, want %v", got, want)
} }
} }
@@ -94,14 +94,14 @@ func testFormValues(t *testing.T, r *http.Request, values values) {
} }
r.ParseForm() r.ParseForm()
if !reflect.DeepEqual(want, r.Form) { if got := r.Form; !reflect.DeepEqual(got, want) {
t.Errorf("Request parameters = %v, want %v", r.Form, want) t.Errorf("Request parameters: %v, want %v", got, want)
} }
} }
func testHeader(t *testing.T, r *http.Request, header string, want string) { func testHeader(t *testing.T, r *http.Request, header string, want string) {
if value := r.Header.Get(header); want != value { if got := r.Header.Get(header); got != want {
t.Errorf("Header %s = %s, want: %s", header, value, want) t.Errorf("Header.Get(%q) returned %s, want %s", header, got, want)
} }
} }
@@ -117,11 +117,10 @@ func testURLParseError(t *testing.T, err error) {
func testBody(t *testing.T, r *http.Request, want string) { func testBody(t *testing.T, r *http.Request, want string) {
b, err := ioutil.ReadAll(r.Body) b, err := ioutil.ReadAll(r.Body)
if err != nil { if err != nil {
t.Errorf("Unable to read body") t.Errorf("Error reading request body: %v", err)
} }
str := string(b) if got := string(b); got != want {
if want != str { t.Errorf("request Body is %s, want %s", got, want)
t.Errorf("Body = %s, want: %s", str, want)
} }
} }
@@ -156,11 +155,11 @@ func testJSONMarshal(t *testing.T, v interface{}, want string) {
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
c := NewClient(nil) c := NewClient(nil)
if c.BaseURL.String() != defaultBaseURL { if got, want := c.BaseURL.String(), defaultBaseURL; got != want {
t.Errorf("NewClient BaseURL = %v, want %v", c.BaseURL.String(), defaultBaseURL) t.Errorf("NewClient BaseURL is %v, want %v", got, want)
} }
if c.UserAgent != userAgent { if got, want := c.UserAgent, userAgent; got != want {
t.Errorf("NewClient UserAgent = %v, want %v", c.UserAgent, userAgent) t.Errorf("NewClient UserAgent is %v, want %v", got, want)
} }
} }
@@ -172,20 +171,19 @@ func TestNewRequest(t *testing.T) {
req, _ := c.NewRequest("GET", inURL, inBody) req, _ := c.NewRequest("GET", inURL, inBody)
// test that relative URL was expanded // test that relative URL was expanded
if req.URL.String() != outURL { if got, want := req.URL.String(), outURL; got != want {
t.Errorf("NewRequest(%v) URL = %v, want %v", inURL, req.URL, outURL) t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want)
} }
// test that body was JSON encoded // test that body was JSON encoded
body, _ := ioutil.ReadAll(req.Body) body, _ := ioutil.ReadAll(req.Body)
if string(body) != outBody { if got, want := string(body), outBody; got != want {
t.Errorf("NewRequest(%v) Body = %v, want %v", inBody, string(body), outBody) t.Errorf("NewRequest(%q) Body is %v, want %v", inBody, got, want)
} }
// test that default user-agent is attached to the request // test that default user-agent is attached to the request
userAgent := req.Header.Get("User-Agent") if got, want := req.Header.Get("User-Agent"), c.UserAgent; got != want {
if c.UserAgent != userAgent { t.Errorf("NewRequest() User-Agent is %v, want %v", got, want)
t.Errorf("NewRequest() User-Agent = %v, want %v", userAgent, c.UserAgent)
} }
} }
@@ -211,6 +209,37 @@ func TestNewRequest_badURL(t *testing.T) {
testURLParseError(t, err) testURLParseError(t, err)
} }
// ensure that no User-Agent header is set if the client's UserAgent is empty.
// This caused a problem with Google's internal http client.
func TestNewRequest_emptyUserAgent(t *testing.T) {
c := NewClient(nil)
c.UserAgent = ""
req, err := c.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("NewRequest returned unexpected error: %v", err)
}
if _, ok := req.Header["User-Agent"]; ok {
t.Fatal("constructed request contains unexpected User-Agent header")
}
}
// If a nil body is passed to github.NewRequest, make sure that nil is also
// passed to http.NewRequest. In most cases, passing an io.Reader that returns
// no content is fine, since there is no difference between an HTTP request
// body that is an empty string versus one that is not set at all. However in
// certain cases, intermediate systems may treat these differently resulting in
// subtle errors.
func TestNewRequest_emptyBody(t *testing.T) {
c := NewClient(nil)
req, err := c.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("NewRequest returned unexpected error: %v", err)
}
if req.Body != nil {
t.Fatalf("constructed request contains a non-nil Body")
}
}
func TestResponse_populatePageValues(t *testing.T) { func TestResponse_populatePageValues(t *testing.T) {
r := http.Response{ r := http.Response{
Header: http.Header{ Header: http.Header{
@@ -223,17 +252,17 @@ func TestResponse_populatePageValues(t *testing.T) {
} }
response := newResponse(&r) response := newResponse(&r)
if want, got := 1, response.FirstPage; want != got { if got, want := response.FirstPage, 1; got != want {
t.Errorf("response.FirstPage: %v, want %v", want, got) t.Errorf("response.FirstPage: %v, want %v", got, want)
} }
if want, got := 2, response.PrevPage; want != got { if got, want := response.PrevPage, 2; want != got {
t.Errorf("response.PrevPage: %v, want %v", want, got) t.Errorf("response.PrevPage: %v, want %v", got, want)
} }
if want, got := 4, response.NextPage; want != got { if got, want := response.NextPage, 4; want != got {
t.Errorf("response.NextPage: %v, want %v", want, got) t.Errorf("response.NextPage: %v, want %v", got, want)
} }
if want, got := 5, response.LastPage; want != got { if got, want := response.LastPage, 5; want != got {
t.Errorf("response.LastPage: %v, want %v", want, got) t.Errorf("response.LastPage: %v, want %v", got, want)
} }
} }
@@ -250,17 +279,17 @@ func TestResponse_populatePageValues_invalid(t *testing.T) {
} }
response := newResponse(&r) response := newResponse(&r)
if want, got := 0, response.FirstPage; want != got { if got, want := response.FirstPage, 0; got != want {
t.Errorf("response.FirstPage: %v, want %v", want, got) t.Errorf("response.FirstPage: %v, want %v", got, want)
} }
if want, got := 0, response.PrevPage; want != got { if got, want := response.PrevPage, 0; got != want {
t.Errorf("response.PrevPage: %v, want %v", want, got) t.Errorf("response.PrevPage: %v, want %v", got, want)
} }
if want, got := 0, response.NextPage; want != got { if got, want := response.NextPage, 0; got != want {
t.Errorf("response.NextPage: %v, want %v", want, got) t.Errorf("response.NextPage: %v, want %v", got, want)
} }
if want, got := 0, response.LastPage; want != got { if got, want := response.LastPage, 0; got != want {
t.Errorf("response.LastPage: %v, want %v", want, got) t.Errorf("response.LastPage: %v, want %v", got, want)
} }
// more invalid URLs // more invalid URLs
@@ -271,8 +300,8 @@ func TestResponse_populatePageValues_invalid(t *testing.T) {
} }
response = newResponse(&r) response = newResponse(&r)
if want, got := 0, response.FirstPage; want != got { if got, want := response.FirstPage, 0; got != want {
t.Errorf("response.FirstPage: %v, want %v", want, got) t.Errorf("response.FirstPage: %v, want %v", got, want)
} }
} }
@@ -349,13 +378,11 @@ func TestDo_rateLimit(t *testing.T) {
w.Header().Add(headerRateReset, "1372700873") w.Header().Add(headerRateReset, "1372700873")
}) })
var want int if got, want := client.Rate.Limit, 0; got != want {
t.Errorf("Client rate limit = %v, want %v", got, want)
if want = 0; client.Rate.Limit != want {
t.Errorf("Client rate limit = %v, want %v", client.Rate.Limit, want)
} }
if want = 0; client.Rate.Limit != want { if got, want := client.Rate.Limit, 0; got != want {
t.Errorf("Client rate remaining = %v, got %v", client.Rate.Remaining, want) t.Errorf("Client rate remaining = %v, got %v", got, want)
} }
if !client.Rate.Reset.IsZero() { if !client.Rate.Reset.IsZero() {
t.Errorf("Client rate reset not initialized to zero value") t.Errorf("Client rate reset not initialized to zero value")
@@ -364,11 +391,11 @@ func TestDo_rateLimit(t *testing.T) {
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest("GET", "/", nil)
client.Do(req, nil) client.Do(req, nil)
if want = 60; client.Rate.Limit != want { if got, want := client.Rate.Limit, 60; got != want {
t.Errorf("Client rate limit = %v, want %v", client.Rate.Limit, want) t.Errorf("Client rate limit = %v, want %v", got, want)
} }
if want = 59; client.Rate.Remaining != want { if got, want := client.Rate.Remaining, 59; got != want {
t.Errorf("Client rate remaining = %v, want %v", client.Rate.Remaining, want) t.Errorf("Client rate remaining = %v, want %v", got, want)
} }
reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC) reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC)
if client.Rate.Reset.UTC() != reset { if client.Rate.Reset.UTC() != reset {
@@ -376,6 +403,7 @@ func TestDo_rateLimit(t *testing.T) {
} }
} }
// ensure rate limit is still parsed, even for error responses
func TestDo_rateLimit_errorResponse(t *testing.T) { func TestDo_rateLimit_errorResponse(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
@@ -387,16 +415,14 @@ func TestDo_rateLimit_errorResponse(t *testing.T) {
http.Error(w, "Bad Request", 400) http.Error(w, "Bad Request", 400)
}) })
var want int
req, _ := client.NewRequest("GET", "/", nil) req, _ := client.NewRequest("GET", "/", nil)
client.Do(req, nil) client.Do(req, nil)
if want = 60; client.Rate.Limit != want { if got, want := client.Rate.Limit, 60; got != want {
t.Errorf("Client rate limit = %v, want %v", client.Rate.Limit, want) t.Errorf("Client rate limit = %v, want %v", got, want)
} }
if want = 59; client.Rate.Remaining != want { if got, want := client.Rate.Remaining, 59; got != want {
t.Errorf("Client rate remaining = %v, want %v", client.Rate.Remaining, want) t.Errorf("Client rate remaining = %v, want %v", got, want)
} }
reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC) reset := time.Date(2013, 7, 1, 17, 47, 53, 0, time.UTC)
if client.Rate.Reset.UTC() != reset { if client.Rate.Reset.UTC() != reset {
@@ -427,8 +453,7 @@ func TestCheckResponse(t *testing.T) {
} }
} }
// ensure that we properly handle API errors that do not contain a response // ensure that we properly handle API errors that do not contain a response body
// body
func TestCheckResponse_noBody(t *testing.T) { func TestCheckResponse_noBody(t *testing.T) {
res := &http.Response{ res := &http.Response{
Request: &http.Request{}, Request: &http.Request{},

View File

@@ -15,7 +15,7 @@ type GitignoresService struct {
client *Client client *Client
} }
// Represents a .gitignore file as returned by the GitHub API. // Gitignore represents a .gitignore file as returned by the GitHub API.
type Gitignore struct { type Gitignore struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Source *string `json:"source,omitempty"` Source *string `json:"source,omitempty"`
@@ -25,7 +25,7 @@ func (g Gitignore) String() string {
return Stringify(g) return Stringify(g)
} }
// Fetches a list of all available Gitignore templates. // List all available Gitignore templates.
// //
// http://developer.github.com/v3/gitignore/#listing-available-templates // http://developer.github.com/v3/gitignore/#listing-available-templates
func (s GitignoresService) List() ([]string, *Response, error) { func (s GitignoresService) List() ([]string, *Response, error) {
@@ -43,7 +43,7 @@ func (s GitignoresService) List() ([]string, *Response, error) {
return *availableTemplates, resp, err return *availableTemplates, resp, err
} }
// Fetches a Gitignore by name. // Get a Gitignore by name.
// //
// http://developer.github.com/v3/gitignore/#get-a-single-template // http://developer.github.com/v3/gitignore/#get-a-single-template
func (s GitignoresService) Get(name string) (*Gitignore, *Response, error) { func (s GitignoresService) Get(name string) (*Gitignore, *Response, error) {

View File

@@ -20,20 +20,25 @@ type IssuesService struct {
// Issue represents a GitHub issue on a repository. // Issue represents a GitHub issue on a repository.
type Issue struct { type Issue struct {
Number *int `json:"number,omitempty"` Number *int `json:"number,omitempty"`
State *string `json:"state,omitempty"` State *string `json:"state,omitempty"`
Title *string `json:"title,omitempty"` Title *string `json:"title,omitempty"`
Body *string `json:"body,omitempty"` Body *string `json:"body,omitempty"`
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
Labels []Label `json:"labels,omitempty"` Labels []Label `json:"labels,omitempty"`
Assignee *User `json:"assignee,omitempty"` Assignee *User `json:"assignee,omitempty"`
Comments *int `json:"comments,omitempty"` Comments *int `json:"comments,omitempty"`
ClosedAt *time.Time `json:"closed_at,omitempty"` ClosedAt *time.Time `json:"closed_at,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
HTMLURL *string `json:"html_url,omitempty"` HTMLURL *string `json:"html_url,omitempty"`
Milestone *Milestone `json:"milestone,omitempty"` Milestone *Milestone `json:"milestone,omitempty"`
PullRequestLinks *PullRequestLinks `json:"pull_request,omitempty"`
// TextMatches is only populated from search results that request text matches
// See: search.go and https://developer.github.com/v3/search/#text-match-metadata
TextMatches []TextMatch `json:"text_matches,omitempty"`
} }
func (i Issue) String() string { func (i Issue) String() string {
@@ -80,6 +85,15 @@ type IssueListOptions struct {
ListOptions ListOptions
} }
// PullRequestLinks object is added to the Issue object when it's an issue included
// in the IssueCommentEvent webhook payload, if the webhooks is fired by a comment on a PR
type PullRequestLinks struct {
URL *string `json:"url,omitempty"`
HTMLURL *string `json:"html_url,omitempty"`
DiffURL *string `json:"diff_url,omitempty"`
PatchURL *string `json:"patch_url,omitempty"`
}
// List the issues for the authenticated user. If all is true, list issues // List the issues for the authenticated user. If all is true, list issues
// across all the user's visible repositories including owned, member, and // across all the user's visible repositories including owned, member, and
// organization repositories; if false, list only owned and member // organization repositories; if false, list only owned and member

View File

@@ -17,6 +17,9 @@ type IssueComment struct {
User *User `json:"user,omitempty"` User *User `json:"user,omitempty"`
CreatedAt *time.Time `json:"created_at,omitempty"` CreatedAt *time.Time `json:"created_at,omitempty"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
URL *string `json:"url,omitempty"`
HTMLURL *string `json:"html_url,omitempty"`
IssueURL *string `json:"issue_url,omitempty"`
} }
func (i IssueComment) String() string { func (i IssueComment) String() string {
@@ -34,6 +37,8 @@ type IssueListCommentsOptions struct {
// Since filters comments by time. // Since filters comments by time.
Since time.Time `url:"since,omitempty"` Since time.Time `url:"since,omitempty"`
ListOptions
} }
// ListComments lists all comments on the specified issue. Specifying an issue // ListComments lists all comments on the specified issue. Specifying an issue

View File

@@ -24,12 +24,16 @@ func TestIssuesService_ListComments_allIssues(t *testing.T) {
"sort": "updated", "sort": "updated",
"direction": "desc", "direction": "desc",
"since": "2002-02-10T15:30:00Z", "since": "2002-02-10T15:30:00Z",
"page": "2",
}) })
fmt.Fprint(w, `[{"id":1}]`) fmt.Fprint(w, `[{"id":1}]`)
}) })
opt := &IssueListCommentsOptions{"updated", "desc", opt := &IssueListCommentsOptions{
time.Date(2002, time.February, 10, 15, 30, 0, 0, time.UTC), Sort: "updated",
Direction: "desc",
Since: time.Date(2002, time.February, 10, 15, 30, 0, 0, time.UTC),
ListOptions: ListOptions{Page: 2},
} }
comments, _, err := client.Issues.ListComments("o", "r", 0, opt) comments, _, err := client.Issues.ListComments("o", "r", 0, opt)
if err != nil { if err != nil {

View File

@@ -7,6 +7,8 @@ package github
import ( import (
"bytes" "bytes"
"fmt"
"net/url"
) )
// MarkdownOptions specifies optional parameters to the Markdown method. // MarkdownOptions specifies optional parameters to the Markdown method.
@@ -48,7 +50,7 @@ func (c *Client) Markdown(text string, opt *MarkdownOptions) (string, *Response,
} }
} }
req, err := c.NewRequest("POST", "/markdown", request) req, err := c.NewRequest("POST", "markdown", request)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -66,7 +68,7 @@ func (c *Client) Markdown(text string, opt *MarkdownOptions) (string, *Response,
// //
// GitHub API docs: https://developer.github.com/v3/emojis/ // GitHub API docs: https://developer.github.com/v3/emojis/
func (c *Client) ListEmojis() (map[string]string, *Response, error) { func (c *Client) ListEmojis() (map[string]string, *Response, error) {
req, err := c.NewRequest("GET", "/emojis", nil) req, err := c.NewRequest("GET", "emojis", nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -104,7 +106,7 @@ type APIMeta struct {
// //
// GitHub API docs: https://developer.github.com/v3/meta/ // GitHub API docs: https://developer.github.com/v3/meta/
func (c *Client) APIMeta() (*APIMeta, *Response, error) { func (c *Client) APIMeta() (*APIMeta, *Response, error) {
req, err := c.NewRequest("GET", "/meta", nil) req, err := c.NewRequest("GET", "meta", nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -117,3 +119,43 @@ func (c *Client) APIMeta() (*APIMeta, *Response, error) {
return meta, resp, nil return meta, resp, nil
} }
// Octocat returns an ASCII art octocat with the specified message in a speech
// bubble. If message is empty, a random zen phrase is used.
func (c *Client) Octocat(message string) (string, *Response, error) {
u := "octocat"
if message != "" {
u = fmt.Sprintf("%s?s=%s", u, url.QueryEscape(message))
}
req, err := c.NewRequest("GET", u, nil)
if err != nil {
return "", nil, err
}
buf := new(bytes.Buffer)
resp, err := c.Do(req, buf)
if err != nil {
return "", resp, err
}
return buf.String(), resp, nil
}
// Zen returns a random line from The Zen of GitHub.
//
// see also: http://warpspire.com/posts/taste/
func (c *Client) Zen() (string, *Response, error) {
req, err := c.NewRequest("GET", "zen", nil)
if err != nil {
return "", nil, err
}
buf := new(bytes.Buffer)
resp, err := c.Do(req, buf)
if err != nil {
return "", resp, err
}
return buf.String(), resp, nil
}

View File

@@ -89,3 +89,49 @@ func TestAPIMeta(t *testing.T) {
t.Errorf("APIMeta returned %+v, want %+v", meta, want) t.Errorf("APIMeta returned %+v, want %+v", meta, want)
} }
} }
func TestOctocat(t *testing.T) {
setup()
defer teardown()
input := "input"
output := "sample text"
mux.HandleFunc("/octocat", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"s": input})
w.Header().Set("Content-Type", "application/octocat-stream")
fmt.Fprint(w, output)
})
got, _, err := client.Octocat(input)
if err != nil {
t.Errorf("Octocat returned error: %v", err)
}
if want := output; got != want {
t.Errorf("Octocat returned %+v, want %+v", got, want)
}
}
func TestZen(t *testing.T) {
setup()
defer teardown()
output := "sample text"
mux.HandleFunc("/zen", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
w.Header().Set("Content-Type", "text/plain;charset=utf-8")
fmt.Fprint(w, output)
})
got, _, err := client.Zen()
if err != nil {
t.Errorf("Zen returned error: %v", err)
}
if want := output; got != want {
t.Errorf("Zen returned %+v, want %+v", got, want)
}
}

View File

@@ -7,6 +7,31 @@ package github
import "fmt" import "fmt"
// Membership represents the status of a user's membership in an organization or team.
type Membership struct {
URL *string `json:"url,omitempty"`
// State is the user's status within the organization or team.
// Possible values are: "active", "pending"
State *string `json:"state,omitempty"`
// TODO(willnorris): add docs
Role *string `json:"role,omitempty"`
// For organization membership, the API URL of the organization.
OrganizationURL *string `json:"organization_url,omitempty"`
// For organization membership, the organization the membership is for.
Organization *Organization `json:"organization,omitempty"`
// For organization membership, the user the membership is for.
User *User `json:"user,omitempty"`
}
func (m Membership) String() string {
return Stringify(m)
}
// ListMembersOptions specifies optional parameters to the // ListMembersOptions specifies optional parameters to the
// OrganizationsService.ListMembers method. // OrganizationsService.ListMembers method.
type ListMembersOptions struct { type ListMembersOptions struct {
@@ -120,3 +145,86 @@ func (s *OrganizationsService) ConcealMembership(org, user string) (*Response, e
return s.client.Do(req, nil) return s.client.Do(req, nil)
} }
// ListOrgMembershipsOptions specifies optional parameters to the
// OrganizationsService.ListOrgMemberships method.
type ListOrgMembershipsOptions struct {
// Filter memberships to include only those withe the specified state.
// Possible values are: "active", "pending".
State string `url:"state,omitempty"`
ListOptions
}
// ListOrgMemberships lists the organization memberships for the authenticated user.
//
// GitHub API docs: https://developer.github.com/v3/orgs/members/#list-your-organization-memberships
func (s *OrganizationsService) ListOrgMemberships(opt *ListOrgMembershipsOptions) ([]Membership, *Response, error) {
u := "user/memberships/orgs"
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
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeMembershipPreview)
var memberships []Membership
resp, err := s.client.Do(req, &memberships)
if err != nil {
return nil, resp, err
}
return memberships, resp, err
}
// GetOrgMembership gets the membership for the authenticated user for the
// specified organization.
//
// GitHub API docs: https://developer.github.com/v3/orgs/members/#get-your-organization-membership
func (s *OrganizationsService) GetOrgMembership(org string) (*Membership, *Response, error) {
u := fmt.Sprintf("user/memberships/orgs/%v", org)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeMembershipPreview)
membership := new(Membership)
resp, err := s.client.Do(req, membership)
if err != nil {
return nil, resp, err
}
return membership, resp, err
}
// EditOrgMembership edits the membership for the authenticated user for the
// specified organization.
//
// GitHub API docs: https://developer.github.com/v3/orgs/members/#edit-your-organization-membership
func (s *OrganizationsService) EditOrgMembership(org string, membership *Membership) (*Membership, *Response, error) {
u := fmt.Sprintf("user/memberships/orgs/%v", org)
req, err := s.client.NewRequest("PATCH", u, membership)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeMembershipPreview)
m := new(Membership)
resp, err := s.client.Do(req, m)
if err != nil {
return nil, resp, err
}
return m, resp, err
}

View File

@@ -6,6 +6,7 @@
package github package github
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"reflect" "reflect"
@@ -209,3 +210,83 @@ func TestOrganizationsService_RemoveMember_invalidOrg(t *testing.T) {
_, err := client.Organizations.RemoveMember("%", "u") _, err := client.Organizations.RemoveMember("%", "u")
testURLParseError(t, err) testURLParseError(t, err)
} }
func TestOrganizationsService_ListOrgMemberships(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/user/memberships/orgs", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeMembershipPreview)
testFormValues(t, r, values{
"state": "active",
"page": "2",
})
fmt.Fprint(w, `[{"url":"u"}]`)
})
opt := &ListOrgMembershipsOptions{
State: "active",
ListOptions: ListOptions{Page: 2},
}
memberships, _, err := client.Organizations.ListOrgMemberships(opt)
if err != nil {
t.Errorf("Organizations.ListOrgMemberships returned error: %v", err)
}
want := []Membership{{URL: String("u")}}
if !reflect.DeepEqual(memberships, want) {
t.Errorf("Organizations.ListOrgMemberships returned %+v, want %+v", memberships, want)
}
}
func TestOrganizationsService_GetOrgMembership(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/user/memberships/orgs/o", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeMembershipPreview)
fmt.Fprint(w, `{"url":"u"}`)
})
membership, _, err := client.Organizations.GetOrgMembership("o")
if err != nil {
t.Errorf("Organizations.GetOrgMembership returned error: %v", err)
}
want := &Membership{URL: String("u")}
if !reflect.DeepEqual(membership, want) {
t.Errorf("Organizations.GetOrgMembership returned %+v, want %+v", membership, want)
}
}
func TestOrganizationsService_EditOrgMembership(t *testing.T) {
setup()
defer teardown()
input := &Membership{State: String("active")}
mux.HandleFunc("/user/memberships/orgs/o", func(w http.ResponseWriter, r *http.Request) {
v := new(Membership)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "PATCH")
testHeader(t, r, "Accept", mediaTypeMembershipPreview)
if !reflect.DeepEqual(v, input) {
t.Errorf("Request body = %+v, want %+v", v, input)
}
fmt.Fprint(w, `{"url":"u"}`)
})
membership, _, err := client.Organizations.EditOrgMembership("o", input)
if err != nil {
t.Errorf("Organizations.EditOrgMembership returned error: %v", err)
}
want := &Membership{URL: String("u")}
if !reflect.DeepEqual(membership, want) {
t.Errorf("Organizations.EditOrgMembership returned %+v, want %+v", membership, want)
}
}

View File

@@ -10,13 +10,14 @@ import "fmt"
// Team represents a team within a GitHub organization. Teams are used to // Team represents a team within a GitHub organization. Teams are used to
// manage access to an organization's repositories. // manage access to an organization's repositories.
type Team struct { type Team struct {
ID *int `json:"id,omitempty"` ID *int `json:"id,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
Slug *string `json:"slug,omitempty"` Slug *string `json:"slug,omitempty"`
Permission *string `json:"permission,omitempty"` Permission *string `json:"permission,omitempty"`
MembersCount *int `json:"members_count,omitempty"` MembersCount *int `json:"members_count,omitempty"`
ReposCount *int `json:"repos_count,omitempty"` ReposCount *int `json:"repos_count,omitempty"`
Organization *Organization `json:"organization,omitempty"`
} }
func (t Team) String() string { func (t Team) String() string {
@@ -251,3 +252,101 @@ func (s *OrganizationsService) RemoveTeamRepo(team int, owner string, repo strin
return s.client.Do(req, nil) return s.client.Do(req, nil)
} }
// ListUserTeams lists a user's teams
// GitHub API docs: https://developer.github.com/v3/orgs/teams/#list-user-teams
func (s *OrganizationsService) ListUserTeams(opt *ListOptions) ([]Team, *Response, error) {
u := "user/teams"
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
}
teams := new([]Team)
resp, err := s.client.Do(req, teams)
if err != nil {
return nil, resp, err
}
return *teams, resp, err
}
// GetTeamMembership returns the membership status for a user in a team.
//
// GitHub API docs: https://developer.github.com/v3/orgs/teams/#get-team-membership
func (s *OrganizationsService) GetTeamMembership(team int, user string) (*Membership, *Response, error) {
u := fmt.Sprintf("teams/%v/memberships/%v", team, user)
req, err := s.client.NewRequest("GET", u, nil)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeMembershipPreview)
t := new(Membership)
resp, err := s.client.Do(req, t)
if err != nil {
return nil, resp, err
}
return t, resp, err
}
// AddTeamMembership adds or invites a user to a team.
//
// In order to add a membership between a user and a team, the authenticated
// user must have 'admin' permissions to the team or be an owner of the
// organization that the team is associated with.
//
// If the user is already a part of the team's organization (meaning they're on
// at least one other team in the organization), this endpoint will add the
// user to the team.
//
// If the user is completely unaffiliated with the team's organization (meaning
// they're on none of the organization's teams), this endpoint will send an
// invitation to the user via email. This newly-created membership will be in
// the "pending" state until the user accepts the invitation, at which point
// the membership will transition to the "active" state and the user will be
// added as a member of the team.
//
// GitHub API docs: https://developer.github.com/v3/orgs/teams/#add-team-membership
func (s *OrganizationsService) AddTeamMembership(team int, user string) (*Membership, *Response, error) {
u := fmt.Sprintf("teams/%v/memberships/%v", team, user)
req, err := s.client.NewRequest("PUT", u, nil)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeMembershipPreview)
t := new(Membership)
resp, err := s.client.Do(req, t)
if err != nil {
return nil, resp, err
}
return t, resp, err
}
// RemoveTeamMembership removes a user from a team.
//
// GitHub API docs: https://developer.github.com/v3/orgs/teams/#remove-team-membership
func (s *OrganizationsService) RemoveTeamMembership(team int, user string) (*Response, error) {
u := fmt.Sprintf("teams/%v/memberships/%v", team, user)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeMembershipPreview)
return s.client.Do(req, nil)
}

View File

@@ -435,3 +435,83 @@ func TestOrganizationsService_RemoveTeamRepo_invalidOwner(t *testing.T) {
_, err := client.Organizations.RemoveTeamRepo(1, "%", "r") _, err := client.Organizations.RemoveTeamRepo(1, "%", "r")
testURLParseError(t, err) testURLParseError(t, err)
} }
func TestOrganizationsService_GetTeamMembership(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/teams/1/memberships/u", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testHeader(t, r, "Accept", mediaTypeMembershipPreview)
fmt.Fprint(w, `{"url":"u", "state":"active"}`)
})
membership, _, err := client.Organizations.GetTeamMembership(1, "u")
if err != nil {
t.Errorf("Organizations.GetTeamMembership returned error: %v", err)
}
want := &Membership{URL: String("u"), State: String("active")}
if !reflect.DeepEqual(membership, want) {
t.Errorf("Organizations.GetTeamMembership returned %+v, want %+v", membership, want)
}
}
func TestOrganizationsService_AddTeamMembership(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/teams/1/memberships/u", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "PUT")
testHeader(t, r, "Accept", mediaTypeMembershipPreview)
fmt.Fprint(w, `{"url":"u", "state":"pending"}`)
})
membership, _, err := client.Organizations.AddTeamMembership(1, "u")
if err != nil {
t.Errorf("Organizations.AddTeamMembership returned error: %v", err)
}
want := &Membership{URL: String("u"), State: String("pending")}
if !reflect.DeepEqual(membership, want) {
t.Errorf("Organizations.AddTeamMembership returned %+v, want %+v", membership, want)
}
}
func TestOrganizationsService_RemoveTeamMembership(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/teams/1/memberships/u", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
testHeader(t, r, "Accept", mediaTypeMembershipPreview)
w.WriteHeader(http.StatusNoContent)
})
_, err := client.Organizations.RemoveTeamMembership(1, "u")
if err != nil {
t.Errorf("Organizations.RemoveTeamMembership returned error: %v", err)
}
}
func TestOrganizationsService_ListUserTeams(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/user/teams", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"page": "1"})
fmt.Fprint(w, `[{"id":1}]`)
})
opt := &ListOptions{Page: 1}
teams, _, err := client.Organizations.ListUserTeams(opt)
if err != nil {
t.Errorf("Organizations.ListUserTeams returned error: %v", err)
}
want := []Team{{ID: Int(1)}}
if !reflect.DeepEqual(teams, want) {
t.Errorf("Organizations.ListUserTeams returned %+v, want %+v", teams, want)
}
}

View File

@@ -37,16 +37,28 @@ type PullRequest struct {
Additions *int `json:"additions,omitempty"` Additions *int `json:"additions,omitempty"`
Deletions *int `json:"deletions,omitempty"` Deletions *int `json:"deletions,omitempty"`
ChangedFiles *int `json:"changed_files,omitempty"` ChangedFiles *int `json:"changed_files,omitempty"`
URL *string `json:"url,omitempty"`
HTMLURL *string `json:"html_url,omitempty"` HTMLURL *string `json:"html_url,omitempty"`
IssueURL *string `json:"issue_url,omitempty"`
StatusesURL *string `json:"statuses_url,omitempty"` StatusesURL *string `json:"statuses_url,omitempty"`
// TODO(willnorris): add head and base once we have a Commit struct defined somewhere Head *PullRequestBranch `json:"head,omitempty"`
Base *PullRequestBranch `json:"base,omitempty"`
} }
func (p PullRequest) String() string { func (p PullRequest) String() string {
return Stringify(p) return Stringify(p)
} }
// PullRequestBranch represents a base or head branch in a GitHub pull request.
type PullRequestBranch struct {
Label *string `json:"label,omitempty"`
Ref *string `json:"ref,omitempty"`
SHA *string `json:"sha,omitempty"`
Repo *Repository `json:"repo,omitempty"`
User *User `json:"user,omitempty"`
}
// PullRequestListOptions specifies the optional parameters to the // PullRequestListOptions specifies the optional parameters to the
// PullRequestsService.List method. // PullRequestsService.List method.
type PullRequestListOptions struct { type PullRequestListOptions struct {
@@ -107,10 +119,19 @@ func (s *PullRequestsService) Get(owner string, repo string, number int) (*PullR
return pull, resp, err return pull, resp, err
} }
// NewPullRequest represents a new pull request to be created.
type NewPullRequest struct {
Title *string `json:"title,omitempty"`
Head *string `json:"head,omitempty"`
Base *string `json:"base,omitempty"`
Body *string `json:"body,omitempty"`
Issue *int `json:"issue,omitempty"`
}
// Create a new pull request on the specified repository. // Create a new pull request on the specified repository.
// //
// GitHub API docs: https://developer.github.com/v3/pulls/#create-a-pull-request // GitHub API docs: https://developer.github.com/v3/pulls/#create-a-pull-request
func (s *PullRequestsService) Create(owner string, repo string, pull *PullRequest) (*PullRequest, *Response, error) { func (s *PullRequestsService) Create(owner string, repo string, pull *NewPullRequest) (*PullRequest, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/pulls", owner, repo) u := fmt.Sprintf("repos/%v/%v/pulls", owner, repo)
req, err := s.client.NewRequest("POST", u, pull) req, err := s.client.NewRequest("POST", u, pull)
if err != nil { if err != nil {
@@ -148,7 +169,7 @@ func (s *PullRequestsService) Edit(owner string, repo string, number int, pull *
// ListCommits lists the commits in a pull request. // ListCommits lists the commits in a pull request.
// //
// GitHub API docs: https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request // GitHub API docs: https://developer.github.com/v3/pulls/#list-commits-on-a-pull-request
func (s *PullRequestsService) ListCommits(owner string, repo string, number int, opt *ListOptions) (*[]Commit, *Response, error) { func (s *PullRequestsService) ListCommits(owner string, repo string, number int, opt *ListOptions) ([]RepositoryCommit, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/pulls/%d/commits", owner, repo, number) u := fmt.Sprintf("repos/%v/%v/pulls/%d/commits", owner, repo, number)
u, err := addOptions(u, opt) u, err := addOptions(u, opt)
if err != nil { if err != nil {
@@ -160,19 +181,19 @@ func (s *PullRequestsService) ListCommits(owner string, repo string, number int,
return nil, nil, err return nil, nil, err
} }
commits := new([]Commit) commits := new([]RepositoryCommit)
resp, err := s.client.Do(req, commits) resp, err := s.client.Do(req, commits)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
return commits, resp, err return *commits, resp, err
} }
// ListFiles lists the files in a pull request. // ListFiles lists the files in a pull request.
// //
// GitHub API docs: https://developer.github.com/v3/pulls/#list-pull-requests-files // GitHub API docs: https://developer.github.com/v3/pulls/#list-pull-requests-files
func (s *PullRequestsService) ListFiles(owner string, repo string, number int, opt *ListOptions) (*[]CommitFile, *Response, error) { func (s *PullRequestsService) ListFiles(owner string, repo string, number int, opt *ListOptions) ([]CommitFile, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/pulls/%d/files", owner, repo, number) u := fmt.Sprintf("repos/%v/%v/pulls/%d/files", owner, repo, number)
u, err := addOptions(u, opt) u, err := addOptions(u, opt)
if err != nil { if err != nil {
@@ -190,7 +211,7 @@ func (s *PullRequestsService) ListFiles(owner string, repo string, number int, o
return nil, resp, err return nil, resp, err
} }
return commitFiles, resp, err return *commitFiles, resp, err
} }
// IsMerged checks if a pull request has been merged. // IsMerged checks if a pull request has been merged.

View File

@@ -37,6 +37,8 @@ type PullRequestListCommentsOptions struct {
// Since filters comments by time. // Since filters comments by time.
Since time.Time `url:"since,omitempty"` Since time.Time `url:"since,omitempty"`
ListOptions
} }
// ListComments lists all comments on the specified pull request. Specifying a // ListComments lists all comments on the specified pull request. Specifying a

View File

@@ -24,12 +24,16 @@ func TestPullRequestsService_ListComments_allPulls(t *testing.T) {
"sort": "updated", "sort": "updated",
"direction": "desc", "direction": "desc",
"since": "2002-02-10T15:30:00Z", "since": "2002-02-10T15:30:00Z",
"page": "2",
}) })
fmt.Fprint(w, `[{"id":1}]`) fmt.Fprint(w, `[{"id":1}]`)
}) })
opt := &PullRequestListCommentsOptions{"updated", "desc", opt := &PullRequestListCommentsOptions{
time.Date(2002, time.February, 10, 15, 30, 0, 0, time.UTC), Sort: "updated",
Direction: "desc",
Since: time.Date(2002, time.February, 10, 15, 30, 0, 0, time.UTC),
ListOptions: ListOptions{Page: 2},
} }
pulls, _, err := client.PullRequests.ListComments("o", "r", 0, opt) pulls, _, err := client.PullRequests.ListComments("o", "r", 0, opt)

View File

@@ -67,6 +67,37 @@ func TestPullRequestsService_Get(t *testing.T) {
} }
} }
func TestPullRequestsService_Get_headAndBase(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/pulls/1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
fmt.Fprint(w, `{"number":1,"head":{"ref":"r2","repo":{"id":2}},"base":{"ref":"r1","repo":{"id":1}}}`)
})
pull, _, err := client.PullRequests.Get("o", "r", 1)
if err != nil {
t.Errorf("PullRequests.Get returned error: %v", err)
}
want := &PullRequest{
Number: Int(1),
Head: &PullRequestBranch{
Ref: String("r2"),
Repo: &Repository{ID: Int(2)},
},
Base: &PullRequestBranch{
Ref: String("r1"),
Repo: &Repository{ID: Int(1)},
},
}
if !reflect.DeepEqual(pull, want) {
t.Errorf("PullRequests.Get returned %+v, want %+v", pull, want)
}
}
func TestPullRequestsService_Get_invalidOwner(t *testing.T) { func TestPullRequestsService_Get_invalidOwner(t *testing.T) {
_, _, err := client.PullRequests.Get("%", "r", 1) _, _, err := client.PullRequests.Get("%", "r", 1)
testURLParseError(t, err) testURLParseError(t, err)
@@ -76,10 +107,10 @@ func TestPullRequestsService_Create(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
input := &PullRequest{Title: String("t")} input := &NewPullRequest{Title: String("t")}
mux.HandleFunc("/repos/o/r/pulls", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/repos/o/r/pulls", func(w http.ResponseWriter, r *http.Request) {
v := new(PullRequest) v := new(NewPullRequest)
json.NewDecoder(r.Body).Decode(v) json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST") testMethod(t, r, "POST")
@@ -174,19 +205,19 @@ func TestPullRequestsService_ListCommits(t *testing.T) {
t.Errorf("PullRequests.ListCommits returned error: %v", err) t.Errorf("PullRequests.ListCommits returned error: %v", err)
} }
want := &[]Commit{ want := []RepositoryCommit{
Commit{ {
SHA: String("3"), SHA: String("3"),
Parents: []Commit{ Parents: []Commit{
Commit{ {
SHA: String("2"), SHA: String("2"),
}, },
}, },
}, },
Commit{ {
SHA: String("2"), SHA: String("2"),
Parents: []Commit{ Parents: []Commit{
Commit{ {
SHA: String("1"), SHA: String("1"),
}, },
}, },
@@ -233,8 +264,8 @@ func TestPullRequestsService_ListFiles(t *testing.T) {
t.Errorf("PullRequests.ListFiles returned error: %v", err) t.Errorf("PullRequests.ListFiles returned error: %v", err)
} }
want := &[]CommitFile{ want := []CommitFile{
CommitFile{ {
SHA: String("6dcb09b5b57875f334f61aebed695e2e4193db5e"), SHA: String("6dcb09b5b57875f334f61aebed695e2e4193db5e"),
Filename: String("file1.txt"), Filename: String("file1.txt"),
Additions: Int(103), Additions: Int(103),
@@ -243,7 +274,7 @@ func TestPullRequestsService_ListFiles(t *testing.T) {
Status: String("added"), Status: String("added"),
Patch: String("@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test"), Patch: String("@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test"),
}, },
CommitFile{ {
SHA: String("f61aebed695e2e4193db5e6dcb09b5b57875f334"), SHA: String("f61aebed695e2e4193db5e6dcb09b5b57875f334"),
Filename: String("file2.txt"), Filename: String("file2.txt"),
Additions: Int(5), Additions: Int(5),

View File

@@ -43,6 +43,7 @@ type Repository struct {
SubscribersCount *int `json:"subscribers_count,omitempty"` SubscribersCount *int `json:"subscribers_count,omitempty"`
WatchersCount *int `json:"watchers_count,omitempty"` WatchersCount *int `json:"watchers_count,omitempty"`
Size *int `json:"size,omitempty"` Size *int `json:"size,omitempty"`
AutoInit *bool `json:"auto_init,omitempty"`
Parent *Repository `json:"parent,omitempty"` Parent *Repository `json:"parent,omitempty"`
Source *Repository `json:"source,omitempty"` Source *Repository `json:"source,omitempty"`
Organization *Organization `json:"organization,omitempty"` Organization *Organization `json:"organization,omitempty"`
@@ -53,6 +54,8 @@ type Repository struct {
HasIssues *bool `json:"has_issues"` HasIssues *bool `json:"has_issues"`
HasWiki *bool `json:"has_wiki"` HasWiki *bool `json:"has_wiki"`
HasDownloads *bool `json:"has_downloads"` HasDownloads *bool `json:"has_downloads"`
// Creating an organization repository. Required for non-owners.
TeamID *int `json:"team_id"`
// API URLs // API URLs
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
@@ -91,6 +94,10 @@ type Repository struct {
TagsURL *string `json:"tags_url,omitempty"` TagsURL *string `json:"tags_url,omitempty"`
TreesURL *string `json:"trees_url,omitempty"` TreesURL *string `json:"trees_url,omitempty"`
TeamsURL *string `json:"teams_url,omitempty"` TeamsURL *string `json:"teams_url,omitempty"`
// TextMatches is only populated from search results that request text matches
// See: search.go and https://developer.github.com/v3/search/#text-match-metadata
TextMatches []TextMatch `json:"text_matches,omitempty"`
} }
func (r Repository) String() string { func (r Repository) String() string {

View File

@@ -20,6 +20,7 @@ type RepositoryCommit struct {
Committer *User `json:"committer,omitempty"` Committer *User `json:"committer,omitempty"`
Parents []Commit `json:"parents,omitempty"` Parents []Commit `json:"parents,omitempty"`
Message *string `json:"message,omitempty"` Message *string `json:"message,omitempty"`
HTMLURL *string `json:"html_url,omitempty"`
// Details about how many changes were made in this commit. Only filled in during GetCommit! // Details about how many changes were made in this commit. Only filled in during GetCommit!
Stats *CommitStats `json:"stats,omitempty"` Stats *CommitStats `json:"stats,omitempty"`
@@ -94,6 +95,8 @@ type CommitsListOptions struct {
// Until when should Commits be included in the response. // Until when should Commits be included in the response.
Until time.Time `url:"until,omitempty"` Until time.Time `url:"until,omitempty"`
ListOptions
} }
// ListCommits lists the commits of a repository. // ListCommits lists the commits of a repository.

View File

@@ -58,11 +58,11 @@ func (r RepositoryContent) String() string {
} }
// Decode decodes the file content if it is base64 encoded. // Decode decodes the file content if it is base64 encoded.
func (c *RepositoryContent) Decode() ([]byte, error) { func (r *RepositoryContent) Decode() ([]byte, error) {
if *c.Encoding != "base64" { if *r.Encoding != "base64" {
return nil, errors.New("Cannot decode non-base64") return nil, errors.New("cannot decode non-base64")
} }
o, err := base64.StdEncoding.DecodeString(*c.Content) o, err := base64.StdEncoding.DecodeString(*r.Content)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -122,7 +122,7 @@ func (s *RepositoriesService) GetContents(owner, repo, path string, opt *Reposit
if directoryUnmarshalError == nil { if directoryUnmarshalError == nil {
return nil, directoryContent, resp, directoryUnmarshalError return nil, directoryContent, resp, directoryUnmarshalError
} }
return nil, nil, resp, fmt.Errorf("Unmarshalling failed for both file and directory content: %s and %s ", fileUnmarshalError, 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 // CreateFile creates a new file in a repository at the given path and returns

View File

@@ -0,0 +1,174 @@
// Copyright 2014 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 (
"encoding/json"
"fmt"
)
// Deployment represents a deployment in a repo
type Deployment struct {
URL *string `json:"url,omitempty"`
ID *int `json:"id,omitempty"`
SHA *string `json:"sha,omitempty"`
Ref *string `json:"ref,omitempty"`
Task *string `json:"task,omitempty"`
Payload json.RawMessage `json:"payload,omitempty"`
Environment *string `json:"environment,omitempty"`
Description *string `json:"description,omitempty"`
Creator *User `json:"creator,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"`
UpdatedAt *Timestamp `json:"pushed_at,omitempty"`
}
// DeploymentRequest represents a deployment request
type DeploymentRequest struct {
Ref *string `json:"ref,omitempty"`
Task *string `json:"task,omitempty"`
AutoMerge *bool `json:"auto_merge,omitempty"`
RequiredContexts []string `json:"required_contexts,omitempty"`
Payload *string `json:"payload,omitempty"`
Environment *string `json:"environment,omitempty"`
Description *string `json:"description,omitempty"`
}
// DeploymentsListOptions specifies the optional parameters to the
// RepositoriesService.ListDeployments method.
type DeploymentsListOptions struct {
// SHA of the Deployment.
SHA string `url:"sha,omitempty"`
// List deployments for a given ref.
Ref string `url:"ref,omitempty"`
// List deployments for a given task.
Task string `url:"task,omitempty"`
// List deployments for a given environment.
Environment string `url:"environment,omitempty"`
ListOptions
}
// ListDeployments lists the deployments of a repository.
//
// GitHub API docs: https://developer.github.com/v3/repos/deployments/#list-deployments
func (s *RepositoriesService) ListDeployments(owner, repo string, opt *DeploymentsListOptions) ([]Deployment, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/deployments", 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
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeDeploymentPreview)
deployments := new([]Deployment)
resp, err := s.client.Do(req, deployments)
if err != nil {
return nil, resp, err
}
return *deployments, resp, err
}
// CreateDeployment creates a new deployment for a repository.
//
// GitHub API docs: https://developer.github.com/v3/repos/deployments/#create-a-deployment
func (s *RepositoriesService) CreateDeployment(owner, repo string, request *DeploymentRequest) (*Deployment, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/deployments", owner, repo)
req, err := s.client.NewRequest("POST", u, request)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeDeploymentPreview)
d := new(Deployment)
resp, err := s.client.Do(req, d)
if err != nil {
return nil, resp, err
}
return d, resp, err
}
// DeploymentStatus represents the status of a
// particular deployment.
type DeploymentStatus struct {
ID *int `json:"id,omitempty"`
State *string `json:"state,omitempty"`
Creator *User `json:"creator,omitempty"`
Description *string `json:"description,omitempty"`
TargetURL *string `json:"target_url,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"`
UpdatedAt *Timestamp `json:"pushed_at,omitempty"`
}
// DeploymentStatusRequest represents a deployment request
type DeploymentStatusRequest struct {
State *string `json:"state,omitempty"`
TargetURL *string `json:"target_url,omitempty"`
Description *string `json:"description,omitempty"`
}
// ListDeploymentStatuses lists the statuses of a given deployment of a repository.
//
// GitHub API docs: https://developer.github.com/v3/repos/deployments/#list-deployment-statuses
func (s *RepositoriesService) ListDeploymentStatuses(owner, repo string, deployment int, opt *ListOptions) ([]DeploymentStatus, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/deployments/%v/statuses", owner, repo, deployment)
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
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeDeploymentPreview)
statuses := new([]DeploymentStatus)
resp, err := s.client.Do(req, statuses)
if err != nil {
return nil, resp, err
}
return *statuses, resp, err
}
// CreateDeploymentStatus creates a new status for a deployment.
//
// GitHub API docs: https://developer.github.com/v3/repos/deployments/#create-a-deployment-status
func (s *RepositoriesService) CreateDeploymentStatus(owner, repo string, deployment int, request *DeploymentStatusRequest) (*DeploymentStatus, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/deployments/%v/statuses", owner, repo, deployment)
req, err := s.client.NewRequest("POST", u, request)
if err != nil {
return nil, nil, err
}
// TODO: remove custom Accept header when this API fully launches
req.Header.Set("Accept", mediaTypeDeploymentPreview)
d := new(DeploymentStatus)
resp, err := s.client.Do(req, d)
if err != nil {
return nil, resp, err
}
return d, resp, err
}

View File

@@ -0,0 +1,87 @@
// Copyright 2014 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 (
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
)
func TestRepositoriesService_ListDeployments(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/deployments", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"environment": "test"})
fmt.Fprint(w, `[{"id":1}, {"id":2}]`)
})
opt := &DeploymentsListOptions{Environment: "test"}
deployments, _, err := client.Repositories.ListDeployments("o", "r", opt)
if err != nil {
t.Errorf("Repositories.ListDeployments returned error: %v", err)
}
want := []Deployment{{ID: Int(1)}, {ID: Int(2)}}
if !reflect.DeepEqual(deployments, want) {
t.Errorf("Repositories.ListDeployments returned %+v, want %+v", deployments, want)
}
}
func TestRepositoriesService_CreateDeployment(t *testing.T) {
setup()
defer teardown()
input := &DeploymentRequest{Ref: String("1111"), Task: String("deploy")}
mux.HandleFunc("/repos/o/r/deployments", func(w http.ResponseWriter, r *http.Request) {
v := new(DeploymentRequest)
json.NewDecoder(r.Body).Decode(v)
testMethod(t, r, "POST")
if !reflect.DeepEqual(v, input) {
t.Errorf("Request body = %+v, want %+v", v, input)
}
fmt.Fprint(w, `{"ref": "1111", "task": "deploy"}`)
})
deployment, _, err := client.Repositories.CreateDeployment("o", "r", input)
if err != nil {
t.Errorf("Repositories.CreateDeployment returned error: %v", err)
}
want := &Deployment{Ref: String("1111"), Task: String("deploy")}
if !reflect.DeepEqual(deployment, want) {
t.Errorf("Repositories.CreateDeployment returned %+v, want %+v", deployment, want)
}
}
func TestRepositoriesService_ListDeploymentStatuses(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/deployments/1/statuses", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"page": "2"})
fmt.Fprint(w, `[{"id":1}, {"id":2}]`)
})
opt := &ListOptions{Page: 2}
statutses, _, err := client.Repositories.ListDeploymentStatuses("o", "r", 1, opt)
if err != nil {
t.Errorf("Repositories.ListDeploymentStatuses returned error: %v", err)
}
want := []DeploymentStatus{{ID: Int(1)}, {ID: Int(2)}}
if !reflect.DeepEqual(statutses, want) {
t.Errorf("Repositories.ListDeploymentStatuses returned %+v, want %+v", statutses, want)
}
}

View File

@@ -13,6 +13,8 @@ type RepositoryListForksOptions struct {
// How to sort the forks list. Possible values are: newest, oldest, // How to sort the forks list. Possible values are: newest, oldest,
// watchers. Default is "newest". // watchers. Default is "newest".
Sort string `url:"sort,omitempty"` Sort string `url:"sort,omitempty"`
ListOptions
} }
// ListForks lists the forks of the specified repository. // ListForks lists the forks of the specified repository.

View File

@@ -18,11 +18,17 @@ func TestRepositoriesService_ListForks(t *testing.T) {
mux.HandleFunc("/repos/o/r/forks", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/repos/o/r/forks", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET") testMethod(t, r, "GET")
testFormValues(t, r, values{"sort": "newest"}) testFormValues(t, r, values{
"sort": "newest",
"page": "3",
})
fmt.Fprint(w, `[{"id":1},{"id":2}]`) fmt.Fprint(w, `[{"id":1},{"id":2}]`)
}) })
opt := &RepositoryListForksOptions{Sort: "newest"} opt := &RepositoryListForksOptions{
Sort: "newest",
ListOptions: ListOptions{Page: 3},
}
repos, _, err := client.Repositories.ListForks("o", "r", opt) repos, _, err := client.Repositories.ListForks("o", "r", opt)
if err != nil { if err != nil {
t.Errorf("Repositories.ListForks returned error: %v", err) t.Errorf("Repositories.ListForks returned error: %v", err)

View File

@@ -15,19 +15,22 @@ import (
// RepositoryRelease represents a GitHub release in a repository. // RepositoryRelease represents a GitHub release in a repository.
type RepositoryRelease struct { type RepositoryRelease struct {
ID *int `json:"id,omitempty"` ID *int `json:"id,omitempty"`
TagName *string `json:"tag_name,omitempty"` TagName *string `json:"tag_name,omitempty"`
TargetCommitish *string `json:"target_commitish,omitempty"` TargetCommitish *string `json:"target_commitish,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Body *string `json:"body,omitempty"` Body *string `json:"body,omitempty"`
Draft *bool `json:"draft,omitempty"` Draft *bool `json:"draft,omitempty"`
Prerelease *bool `json:"prerelease,omitempty"` Prerelease *bool `json:"prerelease,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"` CreatedAt *Timestamp `json:"created_at,omitempty"`
PublishedAt *Timestamp `json:"published_at,omitempty"` PublishedAt *Timestamp `json:"published_at,omitempty"`
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
HTMLURL *string `json:"html_url,omitempty"` HTMLURL *string `json:"html_url,omitempty"`
AssetsURL *string `json:"assets_url,omitempty"` AssetsURL *string `json:"assets_url,omitempty"`
UploadURL *string `json:"upload_url,omitempty"` Assets []ReleaseAsset `json:"assets,omitempty"`
UploadURL *string `json:"upload_url,omitempty"`
ZipballURL *string `json:"zipball_url,omitempty"`
TarballURL *string `json:"tarball_url,omitempty"`
} }
func (r RepositoryRelease) String() string { func (r RepositoryRelease) String() string {
@@ -36,16 +39,18 @@ func (r RepositoryRelease) String() string {
// ReleaseAsset represents a Github release asset in a repository. // ReleaseAsset represents a Github release asset in a repository.
type ReleaseAsset struct { type ReleaseAsset struct {
ID *int `json:"id,omitempty"` ID *int `json:"id,omitempty"`
URL *string `json:"url,omitempty"` URL *string `json:"url,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Label *string `json:"label,omitempty"` Label *string `json:"label,omitempty"`
State *string `json:"state,omitempty"` State *string `json:"state,omitempty"`
ContentType *string `json:"content_type,omitempty"` ContentType *string `json:"content_type,omitempty"`
Size *int `json:"size,omitempty"` Size *int `json:"size,omitempty"`
DownloadCount *int `json:"download_count,omitempty"` DownloadCount *int `json:"download_count,omitempty"`
CreatedAt *Timestamp `json:"created_at,omitempty"` CreatedAt *Timestamp `json:"created_at,omitempty"`
UpdatedAt *Timestamp `json:"updated_at,omitempty"` UpdatedAt *Timestamp `json:"updated_at,omitempty"`
BrowserDownloadURL *string `json:"browser_download_url,omitempty"`
Uploader *User `json:"uploader,omitempty"`
} }
func (r ReleaseAsset) String() string { func (r ReleaseAsset) String() string {
@@ -235,7 +240,7 @@ func (s *RepositoriesService) UploadReleaseAsset(owner, repo string, id int, opt
return nil, nil, err return nil, nil, err
} }
if stat.IsDir() { if stat.IsDir() {
return nil, nil, errors.New("The asset to upload can't be a directory") return nil, nil, errors.New("the asset to upload can't be a directory")
} }
mediaType := mime.TypeByExtension(filepath.Ext(file.Name())) mediaType := mime.TypeByExtension(filepath.Ext(file.Name()))

View File

@@ -116,7 +116,7 @@ func (s *RepositoriesService) ListCodeFrequency(owner, repo string) ([]WeeklySta
resp, err := s.client.Do(req, &weeks) resp, err := s.client.Do(req, &weeks)
// convert int slices into WeeklyStats // convert int slices into WeeklyStats
stats := make([]WeeklyStats, 0) var stats []WeeklyStats
for _, week := range weeks { for _, week := range weeks {
if len(week) != 3 { if len(week) != 3 {
continue continue
@@ -197,7 +197,7 @@ func (s *RepositoriesService) ListPunchCard(owner, repo string) ([]PunchCard, *R
resp, err := s.client.Do(req, &results) resp, err := s.client.Do(req, &results)
// convert int slices into Punchcards // convert int slices into Punchcards
cards := make([]PunchCard, 0) var cards []PunchCard
for _, result := range results { for _, result := range results {
if len(result) != 3 { if len(result) != 3 {
continue continue

View File

@@ -46,7 +46,7 @@ func TestRepositoriesService_ListContributorsStats(t *testing.T) {
} }
want := []ContributorStats{ want := []ContributorStats{
ContributorStats{ {
Author: &Contributor{ Author: &Contributor{
ID: Int(1), ID: Int(1),
}, },

View File

@@ -12,7 +12,8 @@ import (
// RepoStatus represents the status of a repository at a particular reference. // RepoStatus represents the status of a repository at a particular reference.
type RepoStatus struct { type RepoStatus struct {
ID *int `json:"id,omitempty"` ID *int `json:"id,omitempty"`
URL *string `json:"url,omitempty"`
// State is the current state of the repository. Possible values are: // State is the current state of the repository. Possible values are:
// pending, success, error, or failure. // pending, success, error, or failure.
@@ -42,7 +43,7 @@ func (r RepoStatus) String() string {
// //
// GitHub API docs: http://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref // GitHub API docs: http://developer.github.com/v3/repos/statuses/#list-statuses-for-a-specific-ref
func (s *RepositoriesService) ListStatuses(owner, repo, ref string, opt *ListOptions) ([]RepoStatus, *Response, error) { func (s *RepositoriesService) ListStatuses(owner, repo, ref string, opt *ListOptions) ([]RepoStatus, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/statuses/%v", owner, repo, ref) u := fmt.Sprintf("repos/%v/%v/commits/%v/statuses", owner, repo, ref)
u, err := addOptions(u, opt) u, err := addOptions(u, opt)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -73,11 +74,55 @@ func (s *RepositoriesService) CreateStatus(owner, repo, ref string, status *Repo
return nil, nil, err return nil, nil, err
} }
statuses := new(RepoStatus) repoStatus := new(RepoStatus)
resp, err := s.client.Do(req, statuses) resp, err := s.client.Do(req, repoStatus)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
return statuses, resp, err return repoStatus, resp, err
}
// CombinedStatus represents the combined status of a repository at a particular reference.
type CombinedStatus struct {
// State is the combined state of the repository. Possible values are:
// failture, pending, or success.
State *string `json:"state,omitempty"`
Name *string `json:"name,omitempty"`
SHA *string `json:"sha,omitempty"`
TotalCount *int `json:"total_count,omitempty"`
Statuses []RepoStatus `json:"statuses,omitempty"`
CommitURL *string `json:"commit_url,omitempty"`
RepositoryURL *string `json:"repository_url,omitempty"`
}
func (s CombinedStatus) String() string {
return Stringify(s)
}
// GetCombinedStatus returns the combined status of a repository at the specified
// reference. ref can be a SHA, a branch name, or a tag name.
//
// GitHub API docs: https://developer.github.com/v3/repos/statuses/#get-the-combined-status-for-a-specific-ref
func (s *RepositoriesService) GetCombinedStatus(owner, repo, ref string, opt *ListOptions) (*CombinedStatus, *Response, error) {
u := fmt.Sprintf("repos/%v/%v/commits/%v/status", owner, repo, ref)
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
}
status := new(CombinedStatus)
resp, err := s.client.Do(req, status)
if err != nil {
return nil, resp, err
}
return status, resp, err
} }

View File

@@ -17,7 +17,7 @@ func TestRepositoriesService_ListStatuses(t *testing.T) {
setup() setup()
defer teardown() defer teardown()
mux.HandleFunc("/repos/o/r/statuses/r", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/repos/o/r/commits/r/statuses", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET") testMethod(t, r, "GET")
testFormValues(t, r, values{"page": "2"}) testFormValues(t, r, values{"page": "2"})
fmt.Fprint(w, `[{"id":1}]`) fmt.Fprint(w, `[{"id":1}]`)
@@ -72,3 +72,25 @@ func TestRepositoriesService_CreateStatus_invalidOwner(t *testing.T) {
_, _, err := client.Repositories.CreateStatus("%", "r", "r", nil) _, _, err := client.Repositories.CreateStatus("%", "r", "r", nil)
testURLParseError(t, err) testURLParseError(t, err)
} }
func TestRepositoriesService_GetCombinedStatus(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/repos/o/r/commits/r/status", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testFormValues(t, r, values{"page": "2"})
fmt.Fprint(w, `{"state":"success", "statuses":[{"id":1}]}`)
})
opt := &ListOptions{Page: 2}
status, _, err := client.Repositories.GetCombinedStatus("o", "r", "r", opt)
if err != nil {
t.Errorf("Repositories.GetCombinedStatus returned error: %v", err)
}
want := &CombinedStatus{State: String("success"), Statuses: []RepoStatus{{ID: Int(1)}}}
if !reflect.DeepEqual(status, want) {
t.Errorf("Repositories.GetCombinedStatus returned %+v, want %+v", status, want)
}
}

View File

@@ -34,6 +34,9 @@ type SearchOptions struct {
// desc. Default is desc. // desc. Default is desc.
Order string `url:"order,omitempty"` Order string `url:"order,omitempty"`
// Whether to retrieve text match metadata with a query
TextMatch bool `url:"-"`
ListOptions ListOptions
} }
@@ -82,6 +85,25 @@ func (s *SearchService) Users(query string, opt *SearchOptions) (*UsersSearchRes
return result, resp, err return result, resp, err
} }
// Match represents a single text match.
type Match struct {
Text *string `json:"text,omitempty"`
Indices []int `json:"indices,omitempty"`
}
// TextMatch represents a text match for a SearchResult
type TextMatch struct {
ObjectURL *string `json:"object_url,omitempty"`
ObjectType *string `json:"object_type,omitempty"`
Property *string `json:"property,omitempty"`
Fragment *string `json:"fragment,omitempty"`
Matches []Match `json:"matches,omitempty"`
}
func (tm TextMatch) String() string {
return Stringify(tm)
}
// CodeSearchResult represents the result of an code search. // CodeSearchResult represents the result of an code search.
type CodeSearchResult struct { type CodeSearchResult struct {
Total *int `json:"total_count,omitempty"` Total *int `json:"total_count,omitempty"`
@@ -90,11 +112,12 @@ type CodeSearchResult struct {
// CodeResult represents a single search result. // CodeResult represents a single search result.
type CodeResult struct { type CodeResult struct {
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
Path *string `json:"path,omitempty"` Path *string `json:"path,omitempty"`
SHA *string `json:"sha,omitempty"` SHA *string `json:"sha,omitempty"`
HTMLURL *string `json:"html_url,omitempty"` HTMLURL *string `json:"html_url,omitempty"`
Repository *Repository `json:"repository,omitempty"` Repository *Repository `json:"repository,omitempty"`
TextMatches []TextMatch `json:"text_matches,omitempty"`
} }
func (c CodeResult) String() string { func (c CodeResult) String() string {
@@ -125,5 +148,11 @@ func (s *SearchService) search(searchType string, query string, opt *SearchOptio
return nil, err return nil, err
} }
if opt.TextMatch {
// Accept header defaults to "application/vnd.github.v3+json"
// We change it here to fetch back text-match metadata
req.Header.Set("Accept", "application/vnd.github.v3.text-match+json")
}
return s.client.Do(req, result) return s.client.Do(req, result)
} }

View File

@@ -135,3 +135,62 @@ func TestSearchService_Code(t *testing.T) {
t.Errorf("Search.Code returned %+v, want %+v", result, want) t.Errorf("Search.Code returned %+v, want %+v", result, want)
} }
} }
func TestSearchService_CodeTextMatch(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/search/code", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
textMatchResponse := `
{
"total_count": 1,
"items": [
{
"name":"gopher1",
"text_matches": [
{
"fragment": "I'm afraid my friend what you have found\nIs a gopher who lives to feed",
"matches": [
{
"text": "gopher",
"indices": [
14,
21
]
}
]
}
]
}
]
}
`
fmt.Fprint(w, textMatchResponse)
})
opts := &SearchOptions{Sort: "forks", Order: "desc", ListOptions: ListOptions{Page: 2, PerPage: 2}, TextMatch: true}
result, _, err := client.Search.Code("blah", opts)
if err != nil {
t.Errorf("Search.Code returned error: %v", err)
}
wantedCodeResult := CodeResult{
Name: String("gopher1"),
TextMatches: []TextMatch{{
Fragment: String("I'm afraid my friend what you have found\nIs a gopher who lives to feed"),
Matches: []Match{{Text: String("gopher"), Indices: []int{14, 21}}},
},
},
}
want := &CodeSearchResult{
Total: Int(1),
CodeResults: []CodeResult{wantedCodeResult},
}
if !reflect.DeepEqual(result, want) {
t.Errorf("Search.Code returned %+v, want %+v", result, want)
}
}

View File

@@ -55,6 +55,10 @@ type User struct {
ReposURL *string `json:"repos_url,omitempty"` ReposURL *string `json:"repos_url,omitempty"`
StarredURL *string `json:"starred_url,omitempty"` StarredURL *string `json:"starred_url,omitempty"`
SubscriptionsURL *string `json:"subscriptions_url,omitempty"` SubscriptionsURL *string `json:"subscriptions_url,omitempty"`
// TextMatches is only populated from search results that request text matches
// See: search.go and https://developer.github.com/v3/search/#text-match-metadata
TextMatches []TextMatch `json:"text_matches,omitempty"`
} }
func (u User) String() string { func (u User) String() string {

View File

@@ -0,0 +1,64 @@
// Copyright 2014 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 "fmt"
// PromoteSiteAdmin promotes a user to a site administrator of a GitHub Enterprise instance.
//
// GitHub API docs: https://developer.github.com/v3/users/administration/#promote-an-ordinary-user-to-a-site-administrator
func (s *UsersService) PromoteSiteAdmin(user string) (*Response, error) {
u := fmt.Sprintf("users/%v/site_admin", user)
req, err := s.client.NewRequest("PUT", u, nil)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// DemoteSiteAdmin demotes a user from site administrator of a GitHub Enterprise instance.
//
// GitHub API docs: https://developer.github.com/v3/users/administration/#demote-a-site-administrator-to-an-ordinary-user
func (s *UsersService) DemoteSiteAdmin(user string) (*Response, error) {
u := fmt.Sprintf("users/%v/site_admin", user)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// Suspend a user on a GitHub Enterprise instance.
//
// GitHub API docs: https://developer.github.com/v3/users/administration/#suspend-a-user
func (s *UsersService) Suspend(user string) (*Response, error) {
u := fmt.Sprintf("users/%v/suspended", user)
req, err := s.client.NewRequest("PUT", u, nil)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}
// Unsuspend a user on a GitHub Enterprise instance.
//
// GitHub API docs: https://developer.github.com/v3/users/administration/#unsuspend-a-user
func (s *UsersService) Unsuspend(user string) (*Response, error) {
u := fmt.Sprintf("users/%v/suspended", user)
req, err := s.client.NewRequest("DELETE", u, nil)
if err != nil {
return nil, err
}
return s.client.Do(req, nil)
}

View File

@@ -0,0 +1,71 @@
// Copyright 2014 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 (
"net/http"
"testing"
)
func TestUsersService_PromoteSiteAdmin(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/users/u/site_admin", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "PUT")
w.WriteHeader(http.StatusNoContent)
})
_, err := client.Users.PromoteSiteAdmin("u")
if err != nil {
t.Errorf("Users.PromoteSiteAdmin returned error: %v", err)
}
}
func TestUsersService_DemoteSiteAdmin(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/users/u/site_admin", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
w.WriteHeader(http.StatusNoContent)
})
_, err := client.Users.DemoteSiteAdmin("u")
if err != nil {
t.Errorf("Users.DemoteSiteAdmin returned error: %v", err)
}
}
func TestUsersService_Suspend(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/users/u/suspended", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "PUT")
w.WriteHeader(http.StatusNoContent)
})
_, err := client.Users.Suspend("u")
if err != nil {
t.Errorf("Users.Suspend returned error: %v", err)
}
}
func TestUsersService_Unsuspend(t *testing.T) {
setup()
defer teardown()
mux.HandleFunc("/users/u/suspended", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
w.WriteHeader(http.StatusNoContent)
})
_, err := client.Users.Unsuspend("u")
if err != nil {
t.Errorf("Users.Unsuspend returned error: %v", err)
}
}

View File

@@ -33,6 +33,14 @@ import (
var timeType = reflect.TypeOf(time.Time{}) var timeType = reflect.TypeOf(time.Time{})
var encoderType = reflect.TypeOf(new(Encoder)).Elem()
// Encoder is an interface implemented by any type that wishes to encode
// itself into URL values in a non-standard way.
type Encoder interface {
EncodeValues(key string, v *url.Values) error
}
// Values returns the url.Values encoding of v. // Values returns the url.Values encoding of v.
// //
// Values expects to be passed a struct, and traverses it recursively using the // Values expects to be passed a struct, and traverses it recursively using the
@@ -107,14 +115,14 @@ func Values(v interface{}) (url.Values, error) {
} }
values := make(url.Values) values := make(url.Values)
reflectValue(values, val) err := reflectValue(values, val)
return values, nil return values, err
} }
// reflectValue populates the values parameter from the struct fields in val. // reflectValue populates the values parameter from the struct fields in val.
// Embedded structs are followed recursively (using the rules defined in the // Embedded structs are followed recursively (using the rules defined in the
// Values function documentation) breadth-first. // Values function documentation) breadth-first.
func reflectValue(values url.Values, val reflect.Value) { func reflectValue(values url.Values, val reflect.Value) error {
var embedded []reflect.Value var embedded []reflect.Value
typ := val.Type() typ := val.Type()
@@ -144,6 +152,14 @@ func reflectValue(values url.Values, val reflect.Value) {
continue continue
} }
if sv.Type().Implements(encoderType) {
m := sv.Interface().(Encoder)
if err := m.EncodeValues(name, &values); err != nil {
return err
}
continue
}
if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
var del byte var del byte
if opts.Contains("comma") { if opts.Contains("comma") {
@@ -176,8 +192,12 @@ func reflectValue(values url.Values, val reflect.Value) {
} }
for _, f := range embedded { for _, f := range embedded {
reflectValue(values, f) if err := reflectValue(values, f); err != nil {
return err
}
} }
return nil
} }
// valueString returns the string representation of a value. // valueString returns the string representation of a value.

View File

@@ -5,6 +5,7 @@
package query package query
import ( import (
"fmt"
"net/url" "net/url"
"reflect" "reflect"
"testing" "testing"
@@ -188,6 +189,34 @@ func TestValues_invalidInput(t *testing.T) {
} }
} }
type EncodedArgs []string
func (m EncodedArgs) EncodeValues(key string, v *url.Values) error {
for i, arg := range m {
v.Set(fmt.Sprintf("%s.%d", key, i), arg)
}
return nil
}
func TestValues_Marshaler(t *testing.T) {
s := struct {
Args EncodedArgs `url:"arg"`
}{[]string{"a", "b", "c"}}
v, err := Values(s)
if err != nil {
t.Errorf("Values(%q) returned error: %v", s, err)
}
want := url.Values{
"arg.0": {"a"},
"arg.1": {"b"},
"arg.2": {"c"},
}
if !reflect.DeepEqual(want, v) {
t.Errorf("Values(%q) returned %v, want %v", s, v, want)
}
}
func TestTagParsing(t *testing.T) { func TestTagParsing(t *testing.T) {
name, opts := parseTag("field,foobar,foo") name, opts := parseTag("field,foobar,foo")
if name != "field" { if name != "field" {

View File

@@ -66,15 +66,13 @@ func (c *Colorize) Color(v string) string {
result.WriteString(v[m[1]:nm[0]]) result.WriteString(v[m[1]:nm[0]])
m = nm m = nm
// If we're disabled, just ignore the color code information
if c.Disable {
continue
}
var replace string var replace string
if code, ok := c.Colors[v[m[0]+1:m[1]-1]]; ok { if code, ok := c.Colors[v[m[0]+1:m[1]-1]]; ok {
colored = true colored = true
replace = fmt.Sprintf("\033[%sm", code)
if !c.Disable {
replace = fmt.Sprintf("\033[%sm", code)
}
} else { } else {
replace = v[m[0]:m[1]] replace = v[m[0]:m[1]]
} }
@@ -83,7 +81,7 @@ func (c *Colorize) Color(v string) string {
} }
result.WriteString(v[m[1]:]) result.WriteString(v[m[1]:])
if colored && c.Reset { if colored && c.Reset && !c.Disable {
// Write the clear byte at the end // Write the clear byte at the end
result.WriteString("\033[0m") result.WriteString("\033[0m")
} }

View File

@@ -61,15 +61,29 @@ func TestColorizeColor_disable(t *testing.T) {
c := def c := def
c.Disable = true c.Disable = true
input := "[blue]foo" cases := []struct {
output := "foo" Input, Output string
actual := c.Color(input) }{
if actual != output { {
t.Errorf( "[blue]foo",
"Input: %#v\n\nOutput: %#v\n\nExpected: %#v", "foo",
input, },
actual,
output) {
"[foo]bar",
"[foo]bar",
},
}
for _, tc := range cases {
actual := c.Color(tc.Input)
if actual != tc.Output {
t.Errorf(
"Input: %#v\n\nOutput: %#v\n\nExpected: %#v",
tc.Input,
actual,
tc.Output)
}
} }
} }

View File

@@ -23,7 +23,7 @@ func printHelp(args []string, additionalArgs []string) {
} else { } else {
command, found := commandMatching(args[0]) command, found := commandMatching(args[0])
if !found { if !found {
complainAndQuit(fmt.Sprintf("Unkown command: %s", args[0])) complainAndQuit(fmt.Sprintf("Unknown command: %s", args[0]))
} }
usageForCommand(command, true) usageForCommand(command, true)

View File

@@ -64,7 +64,7 @@ func (d *DeltaTracker) Delta(suites []testsuite.TestSuite) (delta Delta, errors
func (d *DeltaTracker) WillRun(suite testsuite.TestSuite) error { func (d *DeltaTracker) WillRun(suite testsuite.TestSuite) error {
s, ok := d.suites[suite.Path] s, ok := d.suites[suite.Path]
if !ok { if !ok {
return fmt.Errorf("unkown suite %s", suite.Path) return fmt.Errorf("unknown suite %s", suite.Path)
} }
return s.MarkAsRunAndRecomputedDependencies(d.maxDepth) return s.MarkAsRunAndRecomputedDependencies(d.maxDepth)

View File

@@ -1,11 +1,11 @@
package codelocation_test package codelocation_test
import ( import (
"runtime"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
"github.com/onsi/ginkgo/internal/codelocation" "github.com/onsi/ginkgo/internal/codelocation"
"github.com/onsi/ginkgo/types" "github.com/onsi/ginkgo/types"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"runtime"
) )
var _ = Describe("CodeLocation", func() { var _ = Describe("CodeLocation", func() {

View File

@@ -1,8 +1,8 @@
package containernode_test package containernode_test
import ( import (
"math/rand"
"github.com/onsi/ginkgo/internal/leafnodes" "github.com/onsi/ginkgo/internal/leafnodes"
"math/rand"
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"

View File

@@ -4,10 +4,13 @@ import (
"math" "math"
"time" "time"
"sync"
"github.com/onsi/ginkgo/types" "github.com/onsi/ginkgo/types"
) )
type benchmarker struct { type benchmarker struct {
mu sync.Mutex
measurements map[string]*types.SpecMeasurement measurements map[string]*types.SpecMeasurement
orderCounter int orderCounter int
} }
@@ -23,6 +26,8 @@ func (b *benchmarker) Time(name string, body func(), info ...interface{}) (elaps
body() body()
elapsedTime = time.Since(t) elapsedTime = time.Since(t)
b.mu.Lock()
defer b.mu.Unlock()
measurement := b.getMeasurement(name, "Fastest Time", "Slowest Time", "Average Time", "s", info...) measurement := b.getMeasurement(name, "Fastest Time", "Slowest Time", "Average Time", "s", info...)
measurement.Results = append(measurement.Results, elapsedTime.Seconds()) measurement.Results = append(measurement.Results, elapsedTime.Seconds())
@@ -31,6 +36,8 @@ func (b *benchmarker) Time(name string, body func(), info ...interface{}) (elaps
func (b *benchmarker) RecordValue(name string, value float64, info ...interface{}) { func (b *benchmarker) RecordValue(name string, value float64, info ...interface{}) {
measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", "", info...) measurement := b.getMeasurement(name, "Smallest", " Largest", " Average", "", info...)
b.mu.Lock()
defer b.mu.Unlock()
measurement.Results = append(measurement.Results, value) measurement.Results = append(measurement.Results, value)
} }
@@ -60,6 +67,8 @@ func (b *benchmarker) getMeasurement(name string, smallestLabel string, largestL
} }
func (b *benchmarker) measurementsReport() map[string]*types.SpecMeasurement { func (b *benchmarker) measurementsReport() map[string]*types.SpecMeasurement {
b.mu.Lock()
defer b.mu.Unlock()
for _, measurement := range b.measurements { for _, measurement := range b.measurements {
measurement.Smallest = math.MaxFloat64 measurement.Smallest = math.MaxFloat64
measurement.Largest = -math.MaxFloat64 measurement.Largest = -math.MaxFloat64

View File

@@ -5,10 +5,10 @@ import (
. "github.com/onsi/ginkgo/internal/leafnodes" . "github.com/onsi/ginkgo/internal/leafnodes"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"time"
"github.com/onsi/ginkgo/internal/codelocation" "github.com/onsi/ginkgo/internal/codelocation"
Failer "github.com/onsi/ginkgo/internal/failer" Failer "github.com/onsi/ginkgo/internal/failer"
"github.com/onsi/ginkgo/types" "github.com/onsi/ginkgo/types"
"time"
) )
var _ = Describe("Measure Nodes", func() { var _ = Describe("Measure Nodes", func() {

View File

@@ -4,11 +4,11 @@ import (
. "github.com/onsi/ginkgo" . "github.com/onsi/ginkgo"
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
"time"
"github.com/onsi/ginkgo/config" "github.com/onsi/ginkgo/config"
. "github.com/onsi/ginkgo/internal/remote" . "github.com/onsi/ginkgo/internal/remote"
st "github.com/onsi/ginkgo/reporters/stenographer" st "github.com/onsi/ginkgo/reporters/stenographer"
"github.com/onsi/ginkgo/types" "github.com/onsi/ginkgo/types"
"time"
) )
var _ = Describe("Aggregator", func() { var _ = Describe("Aggregator", func() {