Repository: xanzy/go-gitlab
Pull request #1353 (2022-01-29 16:37:33)

Add support for iterations
This pull request adds support for GitLab (premium) feature `iterations` in both flavors, `project iterations` and `group iterations`.

GitLab API documentation: 
 - https://docs.gitlab.com/ee/api/iterations.html
 - https://docs.gitlab.com/ee/api/group_iterations.html

New API calls:
- `/api/v4/projects/:id/iterations` for listing project iterations
- `/api/v4/groups/:id/iterations` for listing group iterations

As both calls return the same `iteration` obejects, I added only one example to the testdata directory.


Patch:
diff --git a/gitlab.go b/gitlab.go
index 7c62777..ba7f86a 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -131,6 +131,7 @@ type Client struct {
 	GroupCluster          *GroupClustersService
 	GroupImportExport     *GroupImportExportService
 	GroupIssueBoards      *GroupIssueBoardsService
+	GroupIterations       *GroupIterationsService
 	GroupLabels           *GroupLabelsService
 	GroupMembers          *GroupMembersService
 	GroupMilestones       *GroupMilestonesService
@@ -167,6 +168,7 @@ type Client struct {
 	ProjectAccessTokens   *ProjectAccessTokensService
 	ProjectCluster        *ProjectClustersService
 	ProjectImportExport   *ProjectImportExportService
+	ProjectIterations     *ProjectIterationsService
 	ProjectMembers        *ProjectMembersService
 	ProjectMirrors        *ProjectMirrorService
 	ProjectSnippets       *ProjectSnippetsService
@@ -325,6 +327,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.GroupCluster = &GroupClustersService{client: c}
 	c.GroupImportExport = &GroupImportExportService{client: c}
 	c.GroupIssueBoards = &GroupIssueBoardsService{client: c}
+	c.GroupIterations = &GroupIterationsService{client: c}
 	c.GroupLabels = &GroupLabelsService{client: c}
 	c.GroupMembers = &GroupMembersService{client: c}
 	c.GroupMilestones = &GroupMilestonesService{client: c}
@@ -361,6 +364,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.ProjectAccessTokens = &ProjectAccessTokensService{client: c}
 	c.ProjectCluster = &ProjectClustersService{client: c}
 	c.ProjectImportExport = &ProjectImportExportService{client: c}
+	c.ProjectIterations = &ProjectIterationsService{client: c}
 	c.ProjectMembers = &ProjectMembersService{client: c}
 	c.ProjectMirrors = &ProjectMirrorService{client: c}
 	c.ProjectSnippets = &ProjectSnippetsService{client: c}
diff --git a/group_iterations.go b/group_iterations.go
new file mode 100644
index 0000000..a642091
--- /dev/null
+++ b/group_iterations.go
@@ -0,0 +1,90 @@
+//
+// Copyright 2022, Daniel Steinke
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// IterationsAPI handles communication with the iterations related methods
+// of the GitLab API
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/group_iterations.html
+type GroupIterationsService struct {
+	client *Client
+}
+
+// GroupInteration represents a GitLab iteration.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/group_iterations.html
+type GroupIteration struct {
+	ID          int        `json:"id"`
+	IID         int        `json:"iid"`
+	Sequence    int        `json:"sequence"`
+	GroupID     int        `json:"group_id"`
+	Title       string     `json:"title"`
+	Description string     `json:"description"`
+	State       int        `json:"state"`
+	CreatedAt   *time.Time `json:"created_at"`
+	UpdatedAt   *time.Time `json:"updated_at"`
+	DueDate     *ISOTime   `json:"due_date"`
+	StartDate   *ISOTime   `json:"start_date"`
+	WebURL      string     `json:"web_url"`
+}
+
+func (i GroupIteration) String() string {
+	return Stringify(i)
+}
+
+// ListGroupIterationsOptions contains the available ListGroupIterations()
+// options
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_iterations.html#list-group-iterations
+type ListGroupIterationsOptions struct {
+	ListOptions
+	State            *string `url:"state,omitempty" json:"state,omitempty"`
+	Search           *string `url:"search,omitempty" json:"search,omitempty"`
+	IncludeAncestors *bool   `url:"include_ancestors,omitempty" json:"include_ancestors,omitempty"`
+}
+
+// ListGroupIterations returns a list of group iterations.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_iterations.html#list-group-iterations
+func (s *GroupIterationsService) ListGroupIterations(gid interface{}, opt *ListGroupIterationsOptions, options ...RequestOptionFunc) ([]*GroupIteration, *Response, error) {
+	group, err := parseID(gid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("groups/%s/iterations", PathEscape(group))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var gis []*GroupIteration
+	resp, err := s.client.Do(req, &gis)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return gis, resp, err
+}
diff --git a/project_iterations.go b/project_iterations.go
new file mode 100644
index 0000000..78583ef
--- /dev/null
+++ b/project_iterations.go
@@ -0,0 +1,90 @@
+//
+// Copyright 2022, Daniel Steinke
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// IterationsAPI handles communication with the project iterations related
+// methods of the GitLab API
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/iterations.html
+type ProjectIterationsService struct {
+	client *Client
+}
+
+// ProjectIteration represents a GitLab project iteration.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/iterations.html
+type ProjectIteration struct {
+	ID          int        `json:"id"`
+	IID         int        `json:"iid"`
+	Sequence    int        `json:"sequence"`
+	GroupID     int        `json:"group_id"`
+	Title       string     `json:"title"`
+	Description string     `json:"description"`
+	State       int        `json:"state"`
+	CreatedAt   *time.Time `json:"created_at"`
+	UpdatedAt   *time.Time `json:"updated_at"`
+	DueDate     *ISOTime   `json:"due_date"`
+	StartDate   *ISOTime   `json:"start_date"`
+	WebURL      string     `json:"web_url"`
+}
+
+func (i ProjectIteration) String() string {
+	return Stringify(i)
+}
+
+// ListProjectIterationsOptions contains the available ListProjectIterations()
+// options
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_iterations.html#list-project-iterations
+type ListProjectIterationsOptions struct {
+	ListOptions
+	State            *string `url:"state,omitempty" json:"state,omitempty"`
+	Search           *string `url:"search,omitempty" json:"search,omitempty"`
+	IncludeAncestors *bool   `url:"include_ancestors,omitempty" json:"include_ancestors,omitempty"`
+}
+
+// ListProjectIterations returns a list of projects iterations.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/group_iterations.html#list-project-iterations
+func (i *ProjectIterationsService) ListProjectIterations(pid interface{}, opt *ListProjectIterationsOptions, options ...RequestOptionFunc) ([]*ProjectIteration, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/iterations", PathEscape(project))
+
+	req, err := i.client.NewRequest(http.MethodGet, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var pis []*ProjectIteration
+	resp, err := i.client.Do(req, &pis)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return pis, resp, err
+}


Test patch:
diff --git a/group_iterations_test.go b/group_iterations_test.go
new file mode 100644
index 0000000..4d2030f
--- /dev/null
+++ b/group_iterations_test.go
@@ -0,0 +1,49 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestListGroupIterations(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/groups/5/iterations",
+		func(w http.ResponseWriter, r *http.Request) {
+			testMethod(t, r, http.MethodGet)
+			fmt.Fprintf(w, `[
+        {
+          "id": 53,
+          "iid": 13,
+          "sequence": 1,
+          "group_id": 5,
+          "title": "Iteration II",
+          "description": "Ipsum Lorem ipsum",
+          "state": 2,
+          "web_url": "http://gitlab.example.com/groups/my-group/-/iterations/13"
+        }
+      ]`)
+		})
+
+	iterations, _, err := client.GroupIterations.ListGroupIterations(5, &ListGroupIterationsOptions{})
+	if err != nil {
+		t.Errorf("GroupIterations.ListGroupIterations returned error: %v", err)
+	}
+
+	want := []*GroupIteration{{
+		ID:          53,
+		IID:         13,
+		Sequence:    1,
+		GroupID:     5,
+		Title:       "Iteration II",
+		Description: "Ipsum Lorem ipsum",
+		State:       2,
+		WebURL:      "http://gitlab.example.com/groups/my-group/-/iterations/13",
+	}}
+	if !reflect.DeepEqual(want, iterations) {
+		t.Errorf("GroupIterations.ListGroupIterations returned %+v, want %+v", iterations, want)
+	}
+}
diff --git a/project_iterations_test.go b/project_iterations_test.go
new file mode 100644
index 0000000..ca304c6
--- /dev/null
+++ b/project_iterations_test.go
@@ -0,0 +1,49 @@
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestListProjectIterations(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/42/iterations",
+		func(w http.ResponseWriter, r *http.Request) {
+			testMethod(t, r, http.MethodGet)
+			fmt.Fprintf(w, `[
+        {
+          "id": 53,
+          "iid": 13,
+          "sequence": 1,
+          "group_id": 5,
+          "title": "Iteration II",
+          "description": "Ipsum Lorem ipsum",
+          "state": 2,
+          "web_url": "http://gitlab.example.com/groups/my-group/-/iterations/13"
+        }
+      ]`)
+		})
+
+	iterations, _, err := client.ProjectIterations.ListProjectIterations(42, &ListProjectIterationsOptions{})
+	if err != nil {
+		t.Errorf("GroupIterations.ListGroupIterations returned error: %v", err)
+	}
+
+	want := []*ProjectIteration{{
+		ID:          53,
+		IID:         13,
+		Sequence:    1,
+		GroupID:     5,
+		Title:       "Iteration II",
+		Description: "Ipsum Lorem ipsum",
+		State:       2,
+		WebURL:      "http://gitlab.example.com/groups/my-group/-/iterations/13",
+	}}
+	if !reflect.DeepEqual(want, iterations) {
+		t.Errorf("ProjectIterations.ListProjectIterations returned %+v, want %+v", iterations, want)
+	}
+}
diff --git a/testdata/list_group_iterations.json b/testdata/list_group_iterations.json
new file mode 100644
index 0000000..9d032c0
--- /dev/null
+++ b/testdata/list_group_iterations.json
@@ -0,0 +1,16 @@
+[
+    {
+      "id": 53,
+      "iid": 13,
+      "sequence": 1,
+      "group_id": 5,
+      "title": "Iteration II",
+      "description": "Ipsum Lorem ipsum",
+      "state": 2,
+      "created_at": "2020-01-27T05:07:12.573Z",
+      "updated_at": "2020-01-27T05:07:12.573Z",
+      "due_date": "2020-02-01",
+      "start_date": "2020-02-14",
+      "web_url": "http://gitlab.example.com/groups/my-group/-/iterations/13"
+    }
+]