diff --git a/github/github.go b/github/github.go index 760d729..6e80b47 100644 --- a/github/github.go +++ b/github/github.go @@ -78,6 +78,7 @@ type Client struct { Issues *IssuesService Organizations *OrganizationsService + PullRequests *PullRequestsService Repositories *RepositoriesService Users *UsersService } @@ -102,6 +103,7 @@ func NewClient(httpClient *http.Client) *Client { 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.Users = &UsersService{client: c} return c diff --git a/github/issues.go b/github/issues.go index 2497380..89a2f85 100644 --- a/github/issues.go +++ b/github/issues.go @@ -51,25 +51,25 @@ type IssueComment struct { type IssueListOptions struct { // Filter specifies which issues to list. Possible values are: assigned, // created, mentioned, subscribed, all. Default is "assigned". - Filter string + Filter string // State filters issues based on their state. Possible values are: open, // closed. Default is "open". - State string + State string // Labels filters issues based on their label. - Labels []string + Labels []string // Sort specifies how to sort issues. Possible values are: created, updated, // and comments. Default value is "assigned". - Sort string + Sort string // Direction in which to sort issues. Possible values are: asc, desc. // Default is "asc". Direction string // Since filters issues by time. - Since time.Time + Since time.Time } // List the issues for the authenticated user. If all is true, list issues @@ -132,32 +132,32 @@ type IssueListByRepoOptions struct { // State filters issues based on their state. Possible values are: open, // closed. Default is "open". - State string + State string // Assignee filters issues based on their assignee. Possible values are a // user name, "none" for issues that are not assigned, "*" for issues with // any assigned user. - Assignee string + Assignee string // Assignee filters issues based on their creator. - Creator string + Creator string // Assignee filters issues to those mentioned a specific user. Mentioned string // Labels filters issues based on their label. - Labels []string + Labels []string // Sort specifies how to sort issues. Possible values are: created, updated, // and comments. Default value is "assigned". - Sort string + Sort string // Direction in which to sort issues. Possible values are: asc, desc. // Default is "asc". Direction string // Since filters issues by time. - Since time.Time + Since time.Time } // ListByRepo lists the issues for the specified repository. @@ -262,11 +262,24 @@ func (s *IssuesService) CheckAssignee(owner string, repo string, user string) (b return parseBoolResponse(err) } +// IssueListCommentsOptions specifies the optional parameters to the +// IssuesService.ListComments method. +type IssueListCommentsOptions struct { + // Sort specifies how to sort comments. Possible values are: created, updated. + Sort string + + // Direction in which to sort comments. Possible values are: asc, desc. + Direction string + + // Since filters comments by time. + Since time.Time +} + // ListComments lists all comments on the specified issue. Specifying an issue // number of 0 will return all comments on all issues for the repository. // // GitHub API docs: http://developer.github.com/v3/issues/comments/#list-comments-on-an-issue -func (s *IssuesService) ListComments(owner string, repo string, number int) ([]IssueComment, error) { +func (s *IssuesService) ListComments(owner string, repo string, number int, opt *IssueListCommentsOptions) ([]IssueComment, error) { var u string if number == 0 { u = fmt.Sprintf("repos/%v/%v/issues/comments", owner, repo) @@ -274,6 +287,17 @@ func (s *IssuesService) ListComments(owner string, repo string, number int) ([]I u = fmt.Sprintf("repos/%v/%v/issues/%d/comments", owner, repo, number) } + if opt != nil { + params := url.Values{ + "sort": {opt.Sort}, + "direction": {opt.Direction}, + } + if !opt.Since.IsZero() { + params.Add("since", opt.Since.Format(time.RFC3339)) + } + u += "?" + params.Encode() + } + req, err := s.client.NewRequest("GET", u, nil) if err != nil { return nil, err @@ -326,7 +350,7 @@ func (s *IssuesService) EditComment(owner string, repo string, id int, comment * return c, err } -// DeleteComment updates an issue comment. +// DeleteComment deletes an issue comment. // // GitHub API docs: http://developer.github.com/v3/issues/comments/#delete-a-comment func (s *IssuesService) DeleteComment(owner string, repo string, id int) error { @@ -335,7 +359,6 @@ func (s *IssuesService) DeleteComment(owner string, repo string, id int) error { if err != nil { return err } - _, err = s.client.Do(req, nil) return err } diff --git a/github/issues_test.go b/github/issues_test.go index b084039..02564f8 100644 --- a/github/issues_test.go +++ b/github/issues_test.go @@ -315,10 +315,18 @@ func TestIssuesService_ListComments_allIssues(t *testing.T) { mux.HandleFunc("/repos/o/r/issues/comments", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") + testFormValues(t, r, values{ + "sort": "updated", + "direction": "desc", + "since": "2002-02-10T15:30:00Z", + }) fmt.Fprint(w, `[{"id":1}]`) }) - comments, err := client.Issues.ListComments("o", "r", 0) + opt := &IssueListCommentsOptions{"updated", "desc", + time.Date(2002, time.February, 10, 15, 30, 0, 0, time.UTC), + } + comments, err := client.Issues.ListComments("o", "r", 0, opt) if err != nil { t.Errorf("Issues.ListComments returned error: %v", err) } @@ -338,7 +346,7 @@ func TestIssuesService_ListComments_specificIssue(t *testing.T) { fmt.Fprint(w, `[{"id":1}]`) }) - comments, err := client.Issues.ListComments("o", "r", 1) + comments, err := client.Issues.ListComments("o", "r", 1, nil) if err != nil { t.Errorf("Issues.ListComments returned error: %v", err) } @@ -350,7 +358,7 @@ func TestIssuesService_ListComments_specificIssue(t *testing.T) { } func TestIssuesService_ListComments_invalidOwner(t *testing.T) { - _, err := client.Issues.ListComments("%", "r", 1) + _, err := client.Issues.ListComments("%", "r", 1, nil) testURLParseError(t, err) } diff --git a/github/pulls.go b/github/pulls.go new file mode 100644 index 0000000..d1709ae --- /dev/null +++ b/github/pulls.go @@ -0,0 +1,238 @@ +// Copyright 2013 Google. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package github + +import ( + "fmt" + "net/url" + "time" +) + +// PullRequestsService handles communication with the pull request related +// methods of the GitHub API. +// +// GitHub API docs: http://developer.github.com/v3/pulls/ +type PullRequestsService struct { + client *Client +} + +// PullRequest represents a GitHub pull request on a repository. +type PullRequest struct { + Number int `json:"number,omitempty"` + State string `json:"state,omitempty"` + Title string `json:"title,omitempty"` + Body string `json:"body,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + ClosedAt *time.Time `json:"closed_at,omitempty"` + MergedAt *time.Time `json:"merged_at,omitempty"` + User *User `json:"user,omitempty"` + Merged bool `json:"merged,omitempty"` + Mergeable bool `json:"mergeable,omitempty"` + MergedBy *User `json:"merged_by,omitempty"` + Comments int `json:"comments,omitempty"` + Commits int `json:"commits,omitempty"` + Additions int `json:"additions,omitempty"` + Deletions int `json:"deletions,omitempty"` + ChangedFiles int `json:"changed_files,omitempty"` + + // TODO(willnorris): add head and base once we have a Commit struct defined somewhere +} + +// PullRequestComment represents a comment left on a pull request. +type PullRequestComment struct { + ID int `json:"id,omitempty"` + Body string `json:"body,omitempty"` + Path string `json:"path,omitempty"` + Position int `json:"position,omitempty"` + CommitID string `json:"commit_id,omitempty"` + User *User `json:"user,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` +} + +// PullRequestListOptions specifies the optional parameters to the +// PullRequestsService.List method. +type PullRequestListOptions struct { + // State filters pull requests based on their state. Possible values are: + // open, closed. Default is "open". + State string + + // Head filters pull requests by head user and branch name in the format of: + // "user:ref-name". + Head string + + // Base filters pull requests by base branch name. + Base string +} + +// List the pull requests for the specified repository. +// +// GitHub API docs: http://developer.github.com/v3/pulls/#list-pull-requests +func (s *PullRequestsService) List(owner string, repo string, opt *PullRequestListOptions) ([]PullRequest, error) { + u := fmt.Sprintf("repos/%v/%v/pulls", owner, repo) + if opt != nil { + params := url.Values{ + "state": {opt.State}, + "head": {opt.Head}, + "base": {opt.Base}, + } + u += "?" + params.Encode() + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + pulls := new([]PullRequest) + _, err = s.client.Do(req, pulls) + return *pulls, err +} + +// Get a single pull request. +// +// GitHub API docs: https://developer.github.com/v3/pulls/#get-a-single-pull-request +func (s *PullRequestsService) Get(owner string, repo string, number int) (*PullRequest, error) { + u := fmt.Sprintf("repos/%v/%v/pulls/%d", owner, repo, number) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + pull := new(PullRequest) + _, err = s.client.Do(req, pull) + return pull, err +} + +// Create a new pull request on the specified repository. +// +// GitHub API docs: https://developer.github.com/v3/pulls/#create-a-pull-request +func (s *PullRequestsService) Create(owner string, repo string, pull *PullRequest) (*PullRequest, error) { + u := fmt.Sprintf("repos/%v/%v/pulls", owner, repo) + req, err := s.client.NewRequest("POST", u, pull) + if err != nil { + return nil, err + } + p := new(PullRequest) + _, err = s.client.Do(req, p) + return p, err +} + +// Edit a pull request. +// +// GitHub API docs: https://developer.github.com/v3/pulls/#update-a-pull-request +func (s *PullRequestsService) Edit(owner string, repo string, number int, pull *PullRequest) (*PullRequest, error) { + u := fmt.Sprintf("repos/%v/%v/pulls/%d", owner, repo, number) + req, err := s.client.NewRequest("PATCH", u, pull) + if err != nil { + return nil, err + } + p := new(PullRequest) + _, err = s.client.Do(req, p) + return p, err +} + +// PullRequestListCommentsOptions specifies the optional parameters to the +// PullRequestsService.ListComments method. +type PullRequestListCommentsOptions struct { + // Sort specifies how to sort comments. Possible values are: created, updated. + Sort string + + // Direction in which to sort comments. Possible values are: asc, desc. + Direction string + + // Since filters comments by time. + Since time.Time +} + +// ListComments lists all comments on the specified pull request. Specifying a +// pull request number of 0 will return all comments on all pull requests for +// the repository. +// +// GitHub API docs: https://developer.github.com/v3/pulls/comments/#list-comments-on-a-pull-request +func (s *PullRequestsService) ListComments(owner string, repo string, number int, opt *PullRequestListCommentsOptions) ([]PullRequestComment, error) { + var u string + if number == 0 { + u = fmt.Sprintf("repos/%v/%v/pulls/comments", owner, repo) + } else { + u = fmt.Sprintf("repos/%v/%v/pulls/%d/comments", owner, repo, number) + } + + if opt != nil { + params := url.Values{ + "sort": {opt.Sort}, + "direction": {opt.Direction}, + } + if !opt.Since.IsZero() { + params.Add("since", opt.Since.Format(time.RFC3339)) + } + u += "?" + params.Encode() + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + comments := new([]PullRequestComment) + _, err = s.client.Do(req, comments) + return *comments, err +} + +// GetComment fetches the specified pull request comment. +// +// GitHub API docs: https://developer.github.com/v3/pulls/comments/#get-a-single-comment +func (s *PullRequestsService) GetComment(owner string, repo string, number int) (*PullRequestComment, error) { + u := fmt.Sprintf("repos/%v/%v/pulls/comments/%d", owner, repo, number) + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + comment := new(PullRequestComment) + _, err = s.client.Do(req, comment) + return comment, err +} + +// CreateComment creates a new comment on the specified pull request. +// +// GitHub API docs: https://developer.github.com/v3/pulls/comments/#get-a-single-comment +func (s *PullRequestsService) CreateComment(owner string, repo string, number int, comment *PullRequestComment) (*PullRequestComment, error) { + u := fmt.Sprintf("repos/%v/%v/pulls/%d/comments", owner, repo, number) + req, err := s.client.NewRequest("POST", u, comment) + if err != nil { + return nil, err + } + c := new(PullRequestComment) + _, err = s.client.Do(req, c) + return c, err +} + +// EditComment updates a pull request comment. +// +// GitHub API docs: https://developer.github.com/v3/pulls/comments/#edit-a-comment +func (s *PullRequestsService) EditComment(owner string, repo string, number int, comment *PullRequestComment) (*PullRequestComment, error) { + u := fmt.Sprintf("repos/%v/%v/pulls/comments/%d", owner, repo, number) + req, err := s.client.NewRequest("PATCH", u, comment) + if err != nil { + return nil, err + } + c := new(PullRequestComment) + _, err = s.client.Do(req, c) + return c, err +} + +// DeleteComment deletes a pull request comment. +// +// GitHub API docs: https://developer.github.com/v3/pulls/comments/#delete-a-comment +func (s *PullRequestsService) DeleteComment(owner string, repo string, number int) error { + u := fmt.Sprintf("repos/%v/%v/pulls/comments/%d", owner, repo, number) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return err + } + _, err = s.client.Do(req, nil) + return err +} diff --git a/github/pulls_test.go b/github/pulls_test.go new file mode 100644 index 0000000..e95fa0f --- /dev/null +++ b/github/pulls_test.go @@ -0,0 +1,312 @@ +// Copyright 2013 Google. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +package github + +import ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" + "time" +) + +func TestPullRequestsService_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/pulls", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "state": "closed", + "head": "h", + "base": "b", + }) + fmt.Fprint(w, `[{"number":1}]`) + }) + + opt := &PullRequestListOptions{"closed", "h", "b"} + pulls, err := client.PullRequests.List("o", "r", opt) + + if err != nil { + t.Errorf("PullRequests.List returned error: %v", err) + } + + want := []PullRequest{PullRequest{Number: 1}} + if !reflect.DeepEqual(pulls, want) { + t.Errorf("PullRequests.List returned %+v, want %+v", pulls, want) + } +} + +func TestPullRequestsService_List_invalidOwner(t *testing.T) { + _, err := client.PullRequests.List("%", "r", nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_Get(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}`) + }) + + pull, err := client.PullRequests.Get("o", "r", 1) + + if err != nil { + t.Errorf("PullRequests.Get returned error: %v", err) + } + + want := &PullRequest{Number: 1} + if !reflect.DeepEqual(pull, want) { + t.Errorf("PullRequests.Get returned %+v, want %+v", pull, want) + } +} + +func TestPullRequestsService_Get_invalidOwner(t *testing.T) { + _, err := client.PullRequests.Get("%", "r", 1) + testURLParseError(t, err) +} + +func TestPullRequestsService_Create(t *testing.T) { + setup() + defer teardown() + + input := &PullRequest{Title: "t"} + + mux.HandleFunc("/repos/o/r/pulls", func(w http.ResponseWriter, r *http.Request) { + v := new(PullRequest) + 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, `{"number":1}`) + }) + + pull, err := client.PullRequests.Create("o", "r", input) + if err != nil { + t.Errorf("PullRequests.Create returned error: %v", err) + } + + want := &PullRequest{Number: 1} + if !reflect.DeepEqual(pull, want) { + t.Errorf("PullRequests.Create returned %+v, want %+v", pull, want) + } +} + +func TestPullRequestsService_Create_invalidOwner(t *testing.T) { + _, err := client.PullRequests.Create("%", "r", nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_Edit(t *testing.T) { + setup() + defer teardown() + + input := &PullRequest{Title: "t"} + + mux.HandleFunc("/repos/o/r/pulls/1", func(w http.ResponseWriter, r *http.Request) { + v := new(PullRequest) + json.NewDecoder(r.Body).Decode(v) + + testMethod(t, r, "PATCH") + if !reflect.DeepEqual(v, input) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + fmt.Fprint(w, `{"number":1}`) + }) + + pull, err := client.PullRequests.Edit("o", "r", 1, input) + if err != nil { + t.Errorf("PullRequests.Edit returned error: %v", err) + } + + want := &PullRequest{Number: 1} + if !reflect.DeepEqual(pull, want) { + t.Errorf("PullRequests.Edit returned %+v, want %+v", pull, want) + } +} + +func TestPullRequestsService_Edit_invalidOwner(t *testing.T) { + _, err := client.PullRequests.Edit("%", "r", 1, nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_ListComments_allPulls(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/pulls/comments", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{ + "sort": "updated", + "direction": "desc", + "since": "2002-02-10T15:30:00Z", + }) + fmt.Fprint(w, `[{"id":1}]`) + }) + + opt := &PullRequestListCommentsOptions{"updated", "desc", + time.Date(2002, time.February, 10, 15, 30, 0, 0, time.UTC), + } + pulls, err := client.PullRequests.ListComments("o", "r", 0, opt) + + if err != nil { + t.Errorf("PullRequests.ListComments returned error: %v", err) + } + + want := []PullRequestComment{PullRequestComment{ID: 1}} + if !reflect.DeepEqual(pulls, want) { + t.Errorf("PullRequests.ListComments returned %+v, want %+v", pulls, want) + } +} + +func TestPullRequestsService_ListComments_specificPull(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/pulls/1/comments", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[{"id":1}]`) + }) + + pulls, err := client.PullRequests.ListComments("o", "r", 1, nil) + + if err != nil { + t.Errorf("PullRequests.ListComments returned error: %v", err) + } + + want := []PullRequestComment{PullRequestComment{ID: 1}} + if !reflect.DeepEqual(pulls, want) { + t.Errorf("PullRequests.ListComments returned %+v, want %+v", pulls, want) + } +} + +func TestPullRequestsService_ListComments_invalidOwner(t *testing.T) { + _, err := client.PullRequests.ListComments("%", "r", 1, nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_GetComment(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/pulls/comments/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":1}`) + }) + + comment, err := client.PullRequests.GetComment("o", "r", 1) + + if err != nil { + t.Errorf("PullRequests.GetComment returned error: %v", err) + } + + want := &PullRequestComment{ID: 1} + if !reflect.DeepEqual(comment, want) { + t.Errorf("PullRequests.GetComment returned %+v, want %+v", comment, want) + } +} + +func TestPullRequestsService_GetComment_invalidOwner(t *testing.T) { + _, err := client.PullRequests.GetComment("%", "r", 1) + testURLParseError(t, err) +} + +func TestPullRequestsService_CreateComment(t *testing.T) { + setup() + defer teardown() + + input := &PullRequestComment{Body: "b"} + + mux.HandleFunc("/repos/o/r/pulls/1/comments", func(w http.ResponseWriter, r *http.Request) { + v := new(PullRequestComment) + 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, `{"id":1}`) + }) + + comment, err := client.PullRequests.CreateComment("o", "r", 1, input) + + if err != nil { + t.Errorf("PullRequests.CreateComment returned error: %v", err) + } + + want := &PullRequestComment{ID: 1} + if !reflect.DeepEqual(comment, want) { + t.Errorf("PullRequests.CreateComment returned %+v, want %+v", comment, want) + } +} + +func TestPullRequestsService_CreateComment_invalidOwner(t *testing.T) { + _, err := client.PullRequests.CreateComment("%", "r", 1, nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_EditComment(t *testing.T) { + setup() + defer teardown() + + input := &PullRequestComment{Body: "b"} + + mux.HandleFunc("/repos/o/r/pulls/comments/1", func(w http.ResponseWriter, r *http.Request) { + v := new(PullRequestComment) + json.NewDecoder(r.Body).Decode(v) + + testMethod(t, r, "PATCH") + if !reflect.DeepEqual(v, input) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + fmt.Fprint(w, `{"id":1}`) + }) + + comment, err := client.PullRequests.EditComment("o", "r", 1, input) + + if err != nil { + t.Errorf("PullRequests.EditComment returned error: %v", err) + } + + want := &PullRequestComment{ID: 1} + if !reflect.DeepEqual(comment, want) { + t.Errorf("PullRequests.EditComment returned %+v, want %+v", comment, want) + } +} + +func TestPullRequestsService_EditComment_invalidOwner(t *testing.T) { + _, err := client.PullRequests.EditComment("%", "r", 1, nil) + testURLParseError(t, err) +} + +func TestPullRequestsService_DeleteComment(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/pulls/comments/1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + err := client.PullRequests.DeleteComment("o", "r", 1) + if err != nil { + t.Errorf("PullRequests.DeleteComment returned error: %v", err) + } +} + +func TestPullRequestsService_DeleteComment_invalidOwner(t *testing.T) { + err := client.PullRequests.DeleteComment("%", "r", 1) + testURLParseError(t, err) +}