// Copyright 2013 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 provides a client for using the GitHub API. Access different parts of the GitHub API using the various services on a GitHub Client: client := github.NewClient(nil) // list all organizations for user "willnorris" orgs, err := client.Organizations.List("willnorris", nil) Set optional parameters for an API method by passing an Options object. // list recently updated repositories for org "github" opt := &github.RepositoryListByOrgOptions{Sort: "updated"} repos, err := client.Repositories.ListByOrg("github", opt) Make authenticated API calls by constructing a GitHub client using an OAuth capable http.Client: import "code.google.com/p/goauth2/oauth" // simple OAuth transport if you already have an access token; // see goauth2 library for full usage t := &oauth.Transport{ Token: &oauth.Token{AccessToken: "..."}, } client := github.NewClient(t.Client()) // list all repositories for the authenticated user repos, err := client.Repositories.List(nil) Note that when using an authenticated Client, all calls made by the client will include the specified OAuth token. Therefore, authenticated clients should almost never be shared between different users. The full GitHub API is documented at http://developer.github.com/v3/. */ package github import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" "strconv" "strings" "time" ) const ( libraryVersion = "0.1" defaultBaseURL = "https://api.github.com/" userAgent = "go-github/" + libraryVersion headerRateLimit = "X-RateLimit-Limit" headerRateRemaining = "X-RateLimit-Remaining" headerRateReset = "X-RateLimit-Reset" ) // A Client manages communication with the GitHub API. type Client struct { // HTTP client used to communicate with the API. client *http.Client // Base URL for API requests. Defaults to the public GitHub API, but can be // set to a domain endpoint to use with GitHub Enterprise. BaseURL should // always be specified with a trailing slash. BaseURL *url.URL // User agent used when communicating with the GitHub API. UserAgent string // Rate specifies the current rate limit for the client as determined by the // most recent API call. If the client is used in a multi-user application, // this rate may not always be up-to-date. Call RateLimit() to check the // current rate. Rate Rate // Services used for talking to different parts of the API Issues *IssuesService Organizations *OrganizationsService PullRequests *PullRequestsService Repositories *RepositoriesService Git *GitService Users *UsersService Gists *GistsService Activity *ActivityService } // ListOptions specifies the optional parameters to various List methods that // support pagination. type ListOptions struct { // For paginated result sets, page of results to retrieve. Page int } // NewClient returns a new GitHub API client. If a nil httpClient is // provided, http.DefaultClient will be used. To use API methods which require // authentication, provide an http.Client that will perform the authentication // for you (such as that provided by the goauth2 library). func NewClient(httpClient *http.Client) *Client { if httpClient == nil { httpClient = http.DefaultClient } baseURL, _ := url.Parse(defaultBaseURL) c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent} c.Issues = &IssuesService{client: c} c.Organizations = &OrganizationsService{client: c} c.PullRequests = &PullRequestsService{client: c} c.Repositories = &RepositoriesService{client: c} c.Git = &GitService{client: c} c.Users = &UsersService{client: c} c.Gists = &GistsService{client: c} c.Activity = &ActivityService{client: c} return c } // NewRequest creates an API request. A relative URL can be provided in urlStr, // in which case it is resolved relative to the BaseURL of the Client. // Relative URLs should always be specified without a preceding slash. If // specified, the value pointed to by body is JSON encoded and included as the // request body. func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { rel, err := url.Parse(urlStr) if err != nil { return nil, err } u := c.BaseURL.ResolveReference(rel) buf := new(bytes.Buffer) if body != nil { err := json.NewEncoder(buf).Encode(body) if err != nil { return nil, err } } req, err := http.NewRequest(method, u.String(), buf) if err != nil { return nil, err } req.Header.Add("User-Agent", c.UserAgent) return req, nil } // Response is a GitHub API response. This wraps the standard http.Response // returned from GitHub and provides convenient access to things like // pagination links. type Response struct { *http.Response // These fields provide the page values for paginating through a set of // results. Any or all of these may be set to the zero value for // responses that are not part of a paginated set, or for which there // are no additional pages. NextPage int PrevPage int FirstPage int LastPage int Rate } // newResponse creats a new Response for the provided http.Response. func newResponse(r *http.Response) *Response { response := &Response{Response: r} response.populatePageValues() response.populateRate() return response } // populatePageValues parses the HTTP Link response headers and populates the // various pagination link values in the Reponse. func (r *Response) populatePageValues() { if links, ok := r.Response.Header["Link"]; ok && len(links) > 0 { for _, link := range strings.Split(links[0], ",") { segments := strings.Split(strings.TrimSpace(link), ";") // link must at least have href and rel if len(segments) < 2 { continue } // ensure href is properly formatted if !strings.HasPrefix(segments[0], "<") || !strings.HasSuffix(segments[0], ">") { continue } // try to pull out page parameter url, err := url.Parse(segments[0][1 : len(segments[0])-1]) if err != nil { continue } page := url.Query().Get("page") if page == "" { continue } for _, segment := range segments[1:] { switch strings.TrimSpace(segment) { case `rel="next"`: r.NextPage, _ = strconv.Atoi(page) case `rel="prev"`: r.PrevPage, _ = strconv.Atoi(page) case `rel="first"`: r.FirstPage, _ = strconv.Atoi(page) case `rel="last"`: r.LastPage, _ = strconv.Atoi(page) } } } } } // populateRate parses the rate related headers and populates the response Rate. func (r *Response) populateRate() { if limit := r.Header.Get(headerRateLimit); limit != "" { r.Rate.Limit, _ = strconv.Atoi(limit) } if remaining := r.Header.Get(headerRateRemaining); remaining != "" { r.Rate.Remaining, _ = strconv.Atoi(remaining) } if reset := r.Header.Get(headerRateReset); reset != "" { if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 { r.Rate.Reset = time.Unix(v, 0) } } } // Do sends an API request and returns the API response. The API response is // decoded and stored in the value pointed to by v, or returned as an error if // an API error has occurred. func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) { resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() response := newResponse(resp) c.Rate = response.Rate err = CheckResponse(resp) if err != nil { // even though there was an error, we still return the response // in case the caller wants to inspect it further return response, err } if v != nil { err = json.NewDecoder(resp.Body).Decode(v) } return response, err } /* An ErrorResponse reports one or more errors caused by an API request. GitHub API docs: http://developer.github.com/v3/#client-errors */ type ErrorResponse struct { Response *http.Response // HTTP response that caused this error Message string `json:"message"` // error message Errors []Error `json:"errors"` // more detail on individual errors } func (r *ErrorResponse) Error() string { return fmt.Sprintf("%v %v: %d %v %+v", r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message, r.Errors) } /* An Error reports more details on an individual error in an ErrorResponse. These are the possible validation error codes: missing: resource does not exist missing_field: a required field on a resource has not been set invalid: the formatting of a field is invalid already_exists: another resource has the same valid as this field GitHub API docs: http://developer.github.com/v3/#client-errors */ type Error struct { Resource string `json:"resource"` // resource on which the error occurred Field string `json:"field"` // field on which the error occurred Code string `json:"code"` // validation error code } func (e *Error) Error() string { return fmt.Sprintf("%v error caused by %v field on %v resource", e.Code, e.Field, e.Resource) } // CheckResponse checks the API response for errors, and returns them if // present. A response is considered an error if it has a status code outside // the 200 range. API error responses are expected to have either no response // body, or a JSON response body that maps to ErrorResponse. Any other // response body will be silently ignored. func CheckResponse(r *http.Response) error { if c := r.StatusCode; 200 <= c && c <= 299 { return nil } errorResponse := &ErrorResponse{Response: r} data, err := ioutil.ReadAll(r.Body) if err == nil && data != nil { json.Unmarshal(data, errorResponse) } return errorResponse } // parseBoolResponse determines the boolean result from a GitHub API response. // Several GitHub API methods return boolean responses indicated by the HTTP // status code in the response (true indicated by a 204, false indicated by a // 404). This helper function will determine that result and hide the 404 // error if present. Any other error will be returned through as-is. func parseBoolResponse(err error) (bool, error) { if err == nil { return true, nil } if err, ok := err.(*ErrorResponse); ok && err.Response.StatusCode == http.StatusNotFound { // Simply false. In this one case, we do not pass the error through. return false, nil } // some other real error occurred return false, err } // API response wrapper to a rate limit request. type rateResponse struct { Rate struct { Limit int `json:"limit"` Remaining int `json:"remaining"` Reset int64 `json:"reset"` } `json:"rate"` } // Rate represents the rate limit for the current client. Unauthenticated // requests are limited to 60 per hour. Authenticated requests are limited to // 5,000 per hour. type Rate struct { // The number of requests per hour the client is currently limited to. Limit int // The number of remaining requests the client can make this hour. Remaining int // The time at which the current rate limit will reset. Reset time.Time } // RateLimit returns the rate limit for the current client. func (c *Client) RateLimit() (*Rate, *Response, error) { req, err := c.NewRequest("GET", "rate_limit", nil) if err != nil { return nil, nil, err } response := new(rateResponse) resp, err := c.Do(req, response) if err != nil { return nil, nil, err } rate := &Rate{ Limit: response.Rate.Limit, Remaining: response.Rate.Remaining, Reset: time.Unix(response.Rate.Reset, 0), } return rate, resp, err } /* UnauthenticatedRateLimitedTransport allows you to make unauthenticated calls that need to use a higher rate limit associated with your OAuth application. t := &github.UnauthenticatedRateLimitedTransport{ ClientID: "your app's client ID", ClientSecret: "your app's client secret", } client := github.NewClient(t.Client()) This will append the querystring params client_id=xxx&client_secret=yyy to all requests. See http://developer.github.com/v3/#unauthenticated-rate-limited-requests for more information. */ type UnauthenticatedRateLimitedTransport struct { // ClientID is the GitHub OAuth client ID of the current application, which // can be found by selecting its entry in the list at // https://github.com/settings/applications. ClientID string // ClientSecret is the GitHub OAuth client secret of the current // application. ClientSecret string // Transport is the underlying HTTP transport to use when making requests. // It will default to http.DefaultTransport if nil. Transport http.RoundTripper } // RoundTrip implements the RoundTripper interface. func (t *UnauthenticatedRateLimitedTransport) RoundTrip(req *http.Request) (*http.Response, error) { if t.ClientID == "" { return nil, errors.New("ClientID is empty") } if t.ClientSecret == "" { return nil, errors.New("ClientSecret is empty") } // To set extra querystring params, 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) q := req.URL.Query() q.Set("client_id", t.ClientID) q.Set("client_secret", t.ClientSecret) req.URL.RawQuery = q.Encode() // Make the HTTP request. return t.transport().RoundTrip(req) } // Client returns an *http.Client that makes requests which are subject to the // rate limit of your OAuth application. func (t *UnauthenticatedRateLimitedTransport) Client() *http.Client { return &http.Client{Transport: t} } func (t *UnauthenticatedRateLimitedTransport) transport() http.RoundTripper { if t.Transport != nil { return t.Transport } return http.DefaultTransport } // cloneRequest returns a clone of the provided *http.Request. The clone is a // shallow copy of the struct and its Header map. func cloneRequest(r *http.Request) *http.Request { // shallow copy of the struct r2 := new(http.Request) *r2 = *r // deep copy of the Header r2.Header = make(http.Header) for k, s := range r.Header { r2.Header[k] = s } return r2 }