From 0237c55854e69b56bc1f4141797b96af231e611e Mon Sep 17 00:00:00 2001 From: Glenn Lewis Date: Wed, 15 Jun 2016 11:33:53 -0700 Subject: [PATCH] add support for multiple issue assignees Fixes #362. Change-Id: I39d142b4ef54bf514140a56b5fcbbcfa784f0c7f --- github/github.go | 3 ++ github/issues.go | 8 +++++ github/issues_assignees.go | 46 ++++++++++++++++++++++-- github/issues_assignees_test.go | 63 ++++++++++++++++++++++++++++++++- github/issues_test.go | 2 ++ 5 files changed, 119 insertions(+), 3 deletions(-) diff --git a/github/github.go b/github/github.go index 8639db2..dbe8ce6 100644 --- a/github/github.go +++ b/github/github.go @@ -75,6 +75,9 @@ const ( // https://developer.github.com/changes/2016-04-04-git-signing-api-preview/ mediaTypeGitSigningPreview = "application/vnd.github.cryptographer-preview+json" + + // https://developer.github.com/changes/2016-5-27-multiple-assignees/ + mediaTypeMultipleAssigneesPreview = "application/vnd.github.cerberus-preview+json" ) // A Client manages communication with the GitHub API. diff --git a/github/issues.go b/github/issues.go index d380dd3..b3dedf1 100644 --- a/github/issues.go +++ b/github/issues.go @@ -37,6 +37,7 @@ type Issue struct { PullRequestLinks *PullRequestLinks `json:"pull_request,omitempty"` Repository *Repository `json:"repository,omitempty"` Reactions *Reactions `json:"reactions,omitempty"` + Assignees []*User `json:"assignees,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 @@ -57,6 +58,7 @@ type IssueRequest struct { Assignee *string `json:"assignee,omitempty"` State *string `json:"state,omitempty"` Milestone *int `json:"milestone,omitempty"` + Assignees *[]string `json:"assignees,omitempty"` } // IssueListOptions specifies the optional parameters to the IssuesService.List @@ -243,6 +245,9 @@ func (s *IssuesService) Create(owner string, repo string, issue *IssueRequest) ( return nil, nil, err } + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeMultipleAssigneesPreview) + i := new(Issue) resp, err := s.client.Do(req, i) if err != nil { @@ -262,6 +267,9 @@ func (s *IssuesService) Edit(owner string, repo string, number int, issue *Issue return nil, nil, err } + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeMultipleAssigneesPreview) + i := new(Issue) resp, err := s.client.Do(req, i) if err != nil { diff --git a/github/issues_assignees.go b/github/issues_assignees.go index 6338c22..6fda6ac 100644 --- a/github/issues_assignees.go +++ b/github/issues_assignees.go @@ -11,7 +11,7 @@ import "fmt" // which issues may be assigned. // // GitHub API docs: http://developer.github.com/v3/issues/assignees/#list-assignees -func (s *IssuesService) ListAssignees(owner string, repo string, opt *ListOptions) ([]User, *Response, error) { +func (s *IssuesService) ListAssignees(owner, repo string, opt *ListOptions) ([]User, *Response, error) { u := fmt.Sprintf("repos/%v/%v/assignees", owner, repo) u, err := addOptions(u, opt) if err != nil { @@ -34,7 +34,7 @@ func (s *IssuesService) ListAssignees(owner string, repo string, opt *ListOption // IsAssignee checks if a user is an assignee for the specified repository. // // GitHub API docs: http://developer.github.com/v3/issues/assignees/#check-assignee -func (s *IssuesService) IsAssignee(owner string, repo string, user string) (bool, *Response, error) { +func (s *IssuesService) IsAssignee(owner, repo, user string) (bool, *Response, error) { u := fmt.Sprintf("repos/%v/%v/assignees/%v", owner, repo, user) req, err := s.client.NewRequest("GET", u, nil) if err != nil { @@ -44,3 +44,45 @@ func (s *IssuesService) IsAssignee(owner string, repo string, user string) (bool assignee, err := parseBoolResponse(err) return assignee, resp, err } + +// AddAssignees adds the provided GitHub users as assignees to the issue. +// +// GitHub API docs: https://developer.github.com/v3/issues/assignees/#add-assignees-to-an-issue +func (s *IssuesService) AddAssignees(owner, repo string, number int, assignees []string) (*Issue, *Response, error) { + users := &struct { + Assignees []string `json:"assignees,omitempty"` + }{Assignees: assignees} + u := fmt.Sprintf("repos/%v/%v/issues/%v/assignees", owner, repo, number) + req, err := s.client.NewRequest("POST", u, users) + if err != nil { + return nil, nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeMultipleAssigneesPreview) + + issue := &Issue{} + resp, err := s.client.Do(req, issue) + return issue, resp, err +} + +// RemoveAssignees removes the provided GitHub users as assignees from the issue. +// +// GitHub API docs: https://developer.github.com/v3/issues/assignees/#remove-assignees-from-an-issue +func (s *IssuesService) RemoveAssignees(owner, repo string, number int, assignees []string) (*Issue, *Response, error) { + users := &struct { + Assignees []string `json:"assignees,omitempty"` + }{Assignees: assignees} + u := fmt.Sprintf("repos/%v/%v/issues/%v/assignees", owner, repo, number) + req, err := s.client.NewRequest("DELETE", u, users) + if err != nil { + return nil, nil, err + } + + // TODO: remove custom Accept header when this API fully launches. + req.Header.Set("Accept", mediaTypeMultipleAssigneesPreview) + + issue := &Issue{} + resp, err := s.client.Do(req, issue) + return issue, resp, err +} diff --git a/github/issues_assignees_test.go b/github/issues_assignees_test.go index 63e024d..d64d98e 100644 --- a/github/issues_assignees_test.go +++ b/github/issues_assignees_test.go @@ -6,6 +6,7 @@ package github import ( + "encoding/json" "fmt" "net/http" "reflect" @@ -25,7 +26,7 @@ func TestIssuesService_ListAssignees(t *testing.T) { opt := &ListOptions{Page: 2} assignees, _, err := client.Issues.ListAssignees("o", "r", opt) if err != nil { - t.Errorf("Issues.List returned error: %v", err) + t.Errorf("Issues.ListAssignees returned error: %v", err) } want := []User{{ID: Int(1)}} @@ -96,3 +97,63 @@ func TestIssuesService_IsAssignee_invalidOwner(t *testing.T) { _, _, err := client.Issues.IsAssignee("%", "r", "u") testURLParseError(t, err) } + +func TestIssuesService_AddAssignees(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/issues/1/assignees", func(w http.ResponseWriter, r *http.Request) { + var assignees struct { + Assignees []string `json:"assignees,omitempty"` + } + json.NewDecoder(r.Body).Decode(&assignees) + + testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeMultipleAssigneesPreview) + want := []string{"user1", "user2"} + if !reflect.DeepEqual(assignees.Assignees, want) { + t.Errorf("assignees = %+v, want %+v", assignees, want) + } + fmt.Fprint(w, `{"number":1,"assignees":[{"login":"user1"},{"login":"user2"}]}`) + }) + + got, _, err := client.Issues.AddAssignees("o", "r", 1, []string{"user1", "user2"}) + if err != nil { + t.Errorf("Issues.AddAssignees returned error: %v", err) + } + + want := &Issue{Number: Int(1), Assignees: []*User{{Login: String("user1")}, {Login: String("user2")}}} + if !reflect.DeepEqual(got, want) { + t.Errorf("Issues.AddAssignees = %+v, want %+v", got, want) + } +} + +func TestIssuesService_RemoveAssignees(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/issues/1/assignees", func(w http.ResponseWriter, r *http.Request) { + var assignees struct { + Assignees []string `json:"assignees,omitempty"` + } + json.NewDecoder(r.Body).Decode(&assignees) + + testMethod(t, r, "DELETE") + testHeader(t, r, "Accept", mediaTypeMultipleAssigneesPreview) + want := []string{"user1", "user2"} + if !reflect.DeepEqual(assignees.Assignees, want) { + t.Errorf("assignees = %+v, want %+v", assignees, want) + } + fmt.Fprint(w, `{"number":1,"assignees":[]}`) + }) + + got, _, err := client.Issues.RemoveAssignees("o", "r", 1, []string{"user1", "user2"}) + if err != nil { + t.Errorf("Issues.RemoveAssignees returned error: %v", err) + } + + want := &Issue{Number: Int(1), Assignees: []*User{}} + if !reflect.DeepEqual(got, want) { + t.Errorf("Issues.RemoveAssignees = %+v, want %+v", got, want) + } +} diff --git a/github/issues_test.go b/github/issues_test.go index 657e060..adc363a 100644 --- a/github/issues_test.go +++ b/github/issues_test.go @@ -189,6 +189,7 @@ func TestIssuesService_Create(t *testing.T) { json.NewDecoder(r.Body).Decode(v) testMethod(t, r, "POST") + testHeader(t, r, "Accept", mediaTypeMultipleAssigneesPreview) if !reflect.DeepEqual(v, input) { t.Errorf("Request body = %+v, want %+v", v, input) } @@ -223,6 +224,7 @@ func TestIssuesService_Edit(t *testing.T) { json.NewDecoder(r.Body).Decode(v) testMethod(t, r, "PATCH") + testHeader(t, r, "Accept", mediaTypeMultipleAssigneesPreview) if !reflect.DeepEqual(v, input) { t.Errorf("Request body = %+v, want %+v", v, input) }