Repository: xanzy/go-gitlab
Pull request #705 (2019-10-08 15:38:51)

implement move issue
closes https://github.com/xanzy/go-gitlab/issues/702


Patch:
diff --git a/gitlab.go b/gitlab.go
index 7b0dfc6..9b928de 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -99,7 +99,7 @@ const iso8601 = "2006-01-02"
 func (t ISOTime) MarshalJSON() ([]byte, error) {
 	if y := time.Time(t).Year(); y < 0 || y >= 10000 {
 		// ISO 8901 uses 4 digits for the years
-		return nil, errors.New("json: ISOTime year outside of range [0,9999]")
+		return nil, errors.New("ISOTime.MarshalJSON: year outside of range [0,9999]")
 	}
 
 	b := make([]byte, 0, len(iso8601)+2)
diff --git a/issues.go b/issues.go
index 8d4861d..efa6d50 100644
--- a/issues.go
+++ b/issues.go
@@ -75,7 +75,7 @@ type Issue struct {
 	Assignee          *IssueAssignee   `json:"assignee"`
 	Upvotes           int              `json:"upvotes"`
 	Downvotes         int              `json:"downvotes"`
-	Labels            Labels           `json:"labels"`
+	Labels            []string         `json:"labels"`
 	Title             string           `json:"title"`
 	UpdatedAt         *time.Time       `json:"updated_at"`
 	CreatedAt         *time.Time       `json:"created_at"`
@@ -111,7 +111,7 @@ func (l *Labels) MarshalJSON() ([]byte, error) {
 type ListIssuesOptions struct {
 	ListOptions
 	State            *string    `url:"state,omitempty" json:"state,omitempty"`
-	Labels           *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels           Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	WithLabelDetails *bool      `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
 	Milestone        *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
 	Scope            *string    `url:"scope,omitempty" json:"scope,omitempty"`
@@ -154,7 +154,7 @@ func (s *IssuesService) ListIssues(opt *ListIssuesOptions, options ...OptionFunc
 type ListGroupIssuesOptions struct {
 	ListOptions
 	State           *string    `url:"state,omitempty" json:"state,omitempty"`
-	Labels          *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels          Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	IIDs            []int      `url:"iids[],omitempty" json:"iids,omitempty"`
 	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
 	Scope           *string    `url:"scope,omitempty" json:"scope,omitempty"`
@@ -203,7 +203,7 @@ type ListProjectIssuesOptions struct {
 	ListOptions
 	IIDs             []int      `url:"iids[],omitempty" json:"iids,omitempty"`
 	State            *string    `url:"state,omitempty" json:"state,omitempty"`
-	Labels           *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels           Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	WithLabelDetails *bool      `url:"with_labels_details,omitempty" json:"with_labels_details,omitempty"`
 	Milestone        *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
 	Scope            *string    `url:"scope,omitempty" json:"scope,omitempty"`
@@ -280,7 +280,7 @@ type CreateIssueOptions struct {
 	Confidential                       *bool      `url:"confidential,omitempty" json:"confidential,omitempty"`
 	AssigneeIDs                        []int      `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
 	MilestoneID                        *int       `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
-	Labels                             *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels                             Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	CreatedAt                          *time.Time `url:"created_at,omitempty" json:"created_at,omitempty"`
 	DueDate                            *ISOTime   `url:"due_date,omitempty" json:"due_date,omitempty"`
 	MergeRequestToResolveDiscussionsOf *int       `url:"merge_request_to_resolve_discussions_of,omitempty" json:"merge_request_to_resolve_discussions_of,omitempty"`
@@ -321,7 +321,7 @@ type UpdateIssueOptions struct {
 	Confidential     *bool      `url:"confidential,omitempty" json:"confidential,omitempty"`
 	AssigneeIDs      []int      `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
 	MilestoneID      *int       `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
-	Labels           *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels           Labels     `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	StateEvent       *string    `url:"state_event,omitempty" json:"state_event,omitempty"`
 	UpdatedAt        *time.Time `url:"updated_at,omitempty" json:"updated_at,omitempty"`
 	DueDate          *ISOTime   `url:"due_date,omitempty" json:"due_date,omitempty"`
@@ -372,6 +372,38 @@ func (s *IssuesService) DeleteIssue(pid interface{}, issue int, options ...Optio
 	return s.client.Do(req, nil)
 }
 
+// MoveIssueOptions represents the available MoveIssue() options.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#move-an-issue
+type MoveIssueOptions struct {
+	ToProjectID *int `url:"to_project_id,omitempty" json:"to_project_id,omitempty"`
+}
+
+// MoveIssue updates an existing project issue. This function is also used
+// to mark an issue as closed.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/issues.html#move-an-issue
+func (s *IssuesService) MoveIssue(pid interface{}, issue int, opt *MoveIssueOptions, options ...OptionFunc) (*Issue, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/issues/%d/move", pathEscape(project), issue)
+
+	req, err := s.client.NewRequest("POST", u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	i := new(Issue)
+	resp, err := s.client.Do(req, i)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return i, resp, err
+}
+
 // SubscribeToIssue subscribes the authenticated user to the given issue to
 // receive notifications. If the user is already subscribed to the issue, the
 // status code 304 is returned.
diff --git a/merge_requests.go b/merge_requests.go
index 834484a..918b163 100644
--- a/merge_requests.go
+++ b/merge_requests.go
@@ -50,7 +50,7 @@ type MergeRequest struct {
 	Assignees                 []*BasicUser `json:"assignees"`
 	SourceProjectID           int          `json:"source_project_id"`
 	TargetProjectID           int          `json:"target_project_id"`
-	Labels                    Labels       `json:"labels"`
+	Labels                    []string     `json:"labels"`
 	Description               string       `json:"description"`
 	WorkInProgress            bool         `json:"work_in_progress"`
 	Milestone                 *Milestone   `json:"milestone"`
@@ -136,7 +136,7 @@ type ListMergeRequestsOptions struct {
 	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
 	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
 	View            *string    `url:"view,omitempty" json:"view,omitempty"`
-	Labels          *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels          Labels     `url:"labels,omitempty" json:"labels,omitempty"`
 	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
 	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
 	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
@@ -186,7 +186,7 @@ type ListGroupMergeRequestsOptions struct {
 	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
 	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
 	View            *string    `url:"view,omitempty" json:"view,omitempty"`
-	Labels          *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels          Labels     `url:"labels,omitempty" json:"labels,omitempty"`
 	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
 	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
 	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
@@ -238,7 +238,7 @@ type ListProjectMergeRequestsOptions struct {
 	Sort            *string    `url:"sort,omitempty" json:"sort,omitempty"`
 	Milestone       *string    `url:"milestone,omitempty" json:"milestone,omitempty"`
 	View            *string    `url:"view,omitempty" json:"view,omitempty"`
-	Labels          *Labels    `url:"labels,omitempty" json:"labels,omitempty"`
+	Labels          Labels     `url:"labels,omitempty" json:"labels,omitempty"`
 	CreatedAfter    *time.Time `url:"created_after,omitempty" json:"created_after,omitempty"`
 	CreatedBefore   *time.Time `url:"created_before,omitempty" json:"created_before,omitempty"`
 	UpdatedAfter    *time.Time `url:"updated_after,omitempty" json:"updated_after,omitempty"`
@@ -465,7 +465,7 @@ type CreateMergeRequestOptions struct {
 	Description        *string `url:"description,omitempty" json:"description,omitempty"`
 	SourceBranch       *string `url:"source_branch,omitempty" json:"source_branch,omitempty"`
 	TargetBranch       *string `url:"target_branch,omitempty" json:"target_branch,omitempty"`
-	Labels             *Labels `url:"labels,comma,omitempty" json:"labels,omitempty"`
+	Labels             Labels  `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	AssigneeID         *int    `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
 	AssigneeIDs        []int   `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
 	TargetProjectID    *int    `url:"target_project_id,omitempty" json:"target_project_id,omitempty"`
@@ -511,7 +511,7 @@ type UpdateMergeRequestOptions struct {
 	TargetBranch       *string `url:"target_branch,omitempty" json:"target_branch,omitempty"`
 	AssigneeID         *int    `url:"assignee_id,omitempty" json:"assignee_id,omitempty"`
 	AssigneeIDs        []int   `url:"assignee_ids,omitempty" json:"assignee_ids,omitempty"`
-	Labels             *Labels `url:"labels,comma,omitempty" json:"labels,omitempty"`
+	Labels             Labels  `url:"labels,comma,omitempty" json:"labels,omitempty"`
 	MilestoneID        *int    `url:"milestone_id,omitempty" json:"milestone_id,omitempty"`
 	StateEvent         *string `url:"state_event,omitempty" json:"state_event,omitempty"`
 	RemoveSourceBranch *bool   `url:"remove_source_branch,omitempty" json:"remove_source_branch,omitempty"`
diff --git a/project_members.go b/project_members.go
index 54a25fd..abd964a 100644
--- a/project_members.go
+++ b/project_members.go
@@ -124,7 +124,6 @@ func (s *ProjectMembersService) GetProjectMember(pid interface{}, user int, opti
 type AddProjectMemberOptions struct {
 	UserID      *int              `url:"user_id,omitempty" json:"user_id,omitempty"`
 	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
-	ExpiresAt   *string           `url:"expires_at,omitempty" json:"expires_at"`
 }
 
 // AddProjectMember adds a user to a project team. This is an idempotent
@@ -161,7 +160,6 @@ func (s *ProjectMembersService) AddProjectMember(pid interface{}, opt *AddProjec
 // https://docs.gitlab.com/ce/api/members.html#edit-a-member-of-a-group-or-project
 type EditProjectMemberOptions struct {
 	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
-	ExpiresAt   *string           `url:"expires_at,omitempty" json:"expires_at"`
 }
 
 // EditProjectMember updates a project team member to a specified access level..
diff --git a/projects.go b/projects.go
index 90a1307..da072c5 100644
--- a/projects.go
+++ b/projects.go
@@ -772,7 +772,6 @@ type ProjectMember struct {
 	Name        string           `json:"name"`
 	State       string           `json:"state"`
 	CreatedAt   *time.Time       `json:"created_at"`
-	ExpiresAt   *ISOTime         `json:"expires_at"`
 	AccessLevel AccessLevelValue `json:"access_level"`
 }
 


Test patch:
diff --git a/issues_test.go b/issues_test.go
index 1e79994..5c044ef 100644
--- a/issues_test.go
+++ b/issues_test.go
@@ -6,6 +6,8 @@ import (
 	"net/http"
 	"reflect"
 	"testing"
+
+	"github.com/stretchr/testify/assert"
 )
 
 func TestGetIssue(t *testing.T) {
@@ -50,6 +52,30 @@ func TestDeleteIssue(t *testing.T) {
 	}
 }
 
+func TestMoveIssue(t *testing.T) {
+	mux, server, client := setup()
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/issues/11/move", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		mustWriteHTTPResponse(t, w, "testdata/issue_move.json")
+	})
+
+	issue, _, err := client.Issues.MoveIssue("1", 11, &MoveIssueOptions{ToProjectID: Int(5)})
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	want := &Issue{
+		ID:        92,
+		IID:       11,
+		ProjectID: 5,
+	}
+
+	assert.Equal(t, want.IID, issue.IID)
+	assert.Equal(t, want.ProjectID, issue.ProjectID)
+}
+
 func TestListIssues(t *testing.T) {
 	mux, server, client := setup()
 	defer teardown(server)
diff --git a/merge_requests_test.go b/merge_requests_test.go
index a7baaf5..3edb376 100644
--- a/merge_requests_test.go
+++ b/merge_requests_test.go
@@ -27,7 +27,7 @@ var (
 		WebURL:    "https://gitlab.com/tkuah",
 	}
 	getOpts = GetMergeRequestsOptions{}
-	labels  = Labels{
+	labels  = []string{
 		"GitLab Enterprise Edition",
 		"backend",
 		"database",
diff --git a/testdata/issue_move.json b/testdata/issue_move.json
new file mode 100644
index 0000000..c298ccd
--- /dev/null
+++ b/testdata/issue_move.json
@@ -0,0 +1,61 @@
+{
+  "id": 92,
+  "iid": 11,
+  "project_id": 5,
+  "title": "Sit voluptas tempora quisquam aut doloribus et.",
+  "description": "Repellat voluptas quibusdam voluptatem exercitationem.",
+  "state": "opened",
+  "created_at": "2016-04-05T21:41:45.652Z",
+  "updated_at": "2016-04-07T12:20:17.596Z",
+  "closed_at": null,
+  "closed_by": null,
+  "labels": [],
+  "upvotes": 4,
+  "downvotes": 0,
+  "merge_requests_count": 0,
+  "milestone": null,
+  "assignees": [{
+    "name": "Miss Monserrate Beier",
+    "username": "axel.block",
+    "id": 12,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+    "web_url": "https://gitlab.example.com/axel.block"
+  }],
+  "assignee": {
+    "name": "Miss Monserrate Beier",
+    "username": "axel.block",
+    "id": 12,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/46f6f7dc858ada7be1853f7fb96e81da?s=80&d=identicon",
+    "web_url": "https://gitlab.example.com/axel.block"
+  },
+  "author": {
+    "name": "Kris Steuber",
+    "username": "solon.cremin",
+    "id": 10,
+    "state": "active",
+    "avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
+    "web_url": "https://gitlab.example.com/solon.cremin"
+  },
+  "due_date": null,
+  "web_url": "http://example.com/example/example/issues/11",
+  "time_stats": {
+    "time_estimate": 0,
+    "total_time_spent": 0,
+    "human_time_estimate": null,
+    "human_total_time_spent": null
+  },
+  "confidential": false,
+  "discussion_locked": false,
+  "_links": {
+    "self": "http://example.com/api/v4/projects/1/issues/2",
+    "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+    "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+    "project": "http://example.com/api/v4/projects/1"
+  },
+  "task_completion_status":{
+    "count":0,
+    "completed_count":0
+  }
+}