From 76aa5bccb60d5b52e54ca89cba5eea334572a955 Mon Sep 17 00:00:00 2001 From: Will Norris Date: Tue, 25 Jun 2013 13:08:00 -0700 Subject: [PATCH] first pass at pull request methods This includes most API methods on pull requests (with the exception of listing commits and the merge functions), as well as all of the methods for pull request comments. (Fixes #13) This also fixes a few oversights in the issues API. --- github/github.go | 2 + github/issues.go | 51 +++++-- github/issues_test.go | 14 +- github/pulls.go | 238 ++++++++++++++++++++++++++++++++ github/pulls_test.go | 312 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 600 insertions(+), 17 deletions(-) create mode 100644 github/pulls.go create mode 100644 github/pulls_test.go 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) +}