Repository: xanzy/go-gitlab
Pull request #1433 (2022-03-26 16:42:00)

Add support for Deployments' Merge Requests
Following the following documentation for deployments:
https://docs.gitlab.com/ee/api/deployments.html#list-of-merge-requests-associated-with-a-deployment

A deployment can has merge requests associated with it,
the api for merge requests is the same:

https://docs.gitlab.com/ee/api/merge_requests.html#list-merge-requests

Signed-off-by: Daniela Bento <danibento@overdestiny.com>

svanharmelen: This is still labeled as `WIP`. Let me know when it's rebased and ready to be reviewed...

Patch:
diff --git a/deployments_merge_requests.go b/deployments_merge_requests.go
new file mode 100644
index 0000000..4293478
--- /dev/null
+++ b/deployments_merge_requests.go
@@ -0,0 +1,54 @@
+//
+// Copyright 2022, Daniela Filipe Bento
+//
+// 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"
+)
+
+// DeploymentMergeRequestsService handles communication with the deployment's
+// merge requests related methods of the GitLab API.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/deployments.html#list-of-merge-requests-associated-with-a-deployment
+type DeploymentMergeRequestsService struct {
+	client *Client
+}
+
+// ListDeploymentMergeRequests get the merge requests associated with deployment.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/deployments.html#list-of-merge-requests-associated-with-a-deployment
+func (s *DeploymentMergeRequestsService) ListDeploymentMergeRequests(pid interface{}, deployment int, opts *ListMergeRequestsOptions, options ...RequestOptionFunc) ([]*MergeRequest, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/deployments/%d/merge_requests", PathEscape(project), deployment)
+
+	req, err := s.client.NewRequest(http.MethodGet, u, opts, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var mrs []*MergeRequest
+	resp, err := s.client.Do(req, &mrs)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return mrs, resp, err
+}
diff --git a/examples/applications.go b/examples/applications.go
index 74be8d4..c0b68e7 100644
--- a/examples/applications.go
+++ b/examples/applications.go
@@ -55,5 +55,6 @@ func applicationsExample() {
 	if err != nil {
 		log.Fatal(err)
 	}
+
 	log.Printf("Status code response : %d", resp.StatusCode)
 }
diff --git a/examples/cluster_agents.go b/examples/cluster_agents.go
index 5250095..9b4d925 100644
--- a/examples/cluster_agents.go
+++ b/examples/cluster_agents.go
@@ -17,7 +17,6 @@
 package main
 
 import (
-	"fmt"
 	"log"
 
 	"github.com/xanzy/go-gitlab"
@@ -30,22 +29,23 @@ func clusterAgentsExample() {
 	}
 
 	projectID := 33
+	opt := &gitlab.RegisterAgentOptions{
+		Name: gitlab.String("agent-2"),
+	}
 
 	// Register Cluster Agent
-	clusterAgent, _, err := git.ClusterAgents.RegisterAgent(projectID, &gitlab.RegisterAgentOptions{
-		Name: gitlab.String("agent-2"),
-	})
+	clusterAgent, _, err := git.ClusterAgents.RegisterAgent(projectID, opt)
 	if err != nil {
-		panic(err)
+		log.Fatal(err)
 	}
 
-	fmt.Printf("Cluster Agent: %+v\n", clusterAgent)
+	log.Printf("Cluster Agent: %+v\n", clusterAgent)
 
 	// List Cluster Agents
 	clusterAgents, _, err := git.ClusterAgents.ListAgents(projectID, nil)
 	if err != nil {
-		panic(err)
+		log.Fatal(err)
 	}
 
-	fmt.Printf("Cluster Agents: %+v\n", clusterAgents)
+	log.Printf("Cluster Agents: %+v", clusterAgents)
 }
diff --git a/examples/deployments_merge_requests.go b/examples/deployments_merge_requests.go
new file mode 100644
index 0000000..047b65b
--- /dev/null
+++ b/examples/deployments_merge_requests.go
@@ -0,0 +1,40 @@
+//
+// Copyright 2022, Daniela Filipe Bento
+//
+// 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 main
+
+import (
+	"log"
+
+	gitlab "github.com/xanzy/go-gitlab"
+)
+
+func deploymentExample() {
+	git, err := gitlab.NewClient("yourtokengoeshere")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	opt := &gitlab.ListMergeRequestsOptions{}
+	mergeRequests, _, err := git.DeploymentMergeRequests.ListDeploymentMergeRequests(1, 1, opt)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for _, mergeRequest := range mergeRequests {
+		log.Printf("Found merge request: %v\n", mergeRequest)
+	}
+}
diff --git a/examples/impersonation.go b/examples/impersonation.go
index 43215d3..c93f0b6 100644
--- a/examples/impersonation.go
+++ b/examples/impersonation.go
@@ -36,11 +36,11 @@ func impersonationExample() {
 		&gitlab.GetAllImpersonationTokensOptions{State: gitlab.String("active")},
 	)
 	if err != nil {
-		panic(err)
+		log.Fatal(err)
 	}
 
 	for _, token := range tokens {
-		log.Println(token.Token)
+		log.Printf("Found token: %s", token.Token)
 	}
 
 	//create an impersonation token of an user
@@ -49,8 +49,8 @@ func impersonationExample() {
 		&gitlab.CreateImpersonationTokenOptions{Scopes: &[]string{"api"}},
 	)
 	if err != nil {
-		panic(err)
+		log.Fatal(err)
 	}
 
-	log.Println(token.Token)
+	log.Printf("Created token: %s", token.Token)
 }
diff --git a/examples/labels.go b/examples/labels.go
index 678d69f..bd76062 100644
--- a/examples/labels.go
+++ b/examples/labels.go
@@ -29,11 +29,11 @@ func labelExample() {
 	}
 
 	// Create new label
-	l := &gitlab.CreateLabelOptions{
+	opt := &gitlab.CreateLabelOptions{
 		Name:  gitlab.String("My Label"),
 		Color: gitlab.String("#11FF22"),
 	}
-	label, _, err := git.Labels.CreateLabel("myname/myproject", l)
+	label, _, err := git.Labels.CreateLabel("myname/myproject", opt)
 	if err != nil {
 		log.Fatal(err)
 	}
diff --git a/examples/pagination.go b/examples/pagination.go
index 9fc828d..3d4d62f 100644
--- a/examples/pagination.go
+++ b/examples/pagination.go
@@ -17,7 +17,6 @@
 package main
 
 import (
-	"fmt"
 	"log"
 
 	"github.com/xanzy/go-gitlab"
@@ -45,7 +44,7 @@ func pagination() {
 
 		// List all the projects we've found so far.
 		for _, p := range ps {
-			fmt.Printf("Found project: %s", p.Name)
+			log.Printf("Found project: %s", p.Name)
 		}
 
 		// Exit the loop when we've seen all pages.
diff --git a/examples/personal_access_tokens.go b/examples/personal_access_tokens.go
index 07cfa75..b4d93be 100644
--- a/examples/personal_access_tokens.go
+++ b/examples/personal_access_tokens.go
@@ -59,5 +59,5 @@ func patListExampleWithUserFilter() {
 		log.Fatal(err)
 	}
 
-	fmt.Printf("%s\n", data)
+	log.Printf("Found personal access tokens: %s", data)
 }
diff --git a/examples/topics.go b/examples/topics.go
index ef4f09d..ffc5c52 100644
--- a/examples/topics.go
+++ b/examples/topics.go
@@ -17,7 +17,6 @@
 package main
 
 import (
-	"fmt"
 	"log"
 	"os"
 
@@ -39,7 +38,7 @@ func topicExample() {
 		panic(err)
 	}
 
-	fmt.Printf("Topic: %+v\n", topic)
+	log.Printf("Topic: %+v\n", topic)
 
 	// Set topic avatar
 	avatarFile, err := os.Open("5746961_detect_direction_gps_location_map_icon.png")
@@ -55,7 +54,7 @@ func topicExample() {
 	if err != nil {
 		panic(err)
 	}
-	fmt.Printf("Topic with Avatar: %+v\n", topic)
+	log.Printf("Topic with Avatar: %+v\n", topic)
 
 	// Remove topic avatar
 	topic, _, err = git.Topics.UpdateTopic(topic.ID, &gitlab.UpdateTopicOptions{
@@ -65,5 +64,5 @@ func topicExample() {
 		panic(err)
 	}
 
-	fmt.Printf("Topic without Avatar: %+v\n", topic)
+	log.Printf("Topic without Avatar: %+v\n", topic)
 }
diff --git a/examples/webhook.go b/examples/webhook.go
index 655437e..471272c 100644
--- a/examples/webhook.go
+++ b/examples/webhook.go
@@ -39,14 +39,14 @@ func (hook webhook) ServeHTTP(writer http.ResponseWriter, request *http.Request)
 	event, err := hook.parse(request)
 	if err != nil {
 		writer.WriteHeader(500)
-		writer.Write([]byte(fmt.Sprintf("could parse the webhook event: %v", err)))
+		fmt.Fprintf(writer, "could parse the webhook event: %v", err)
 		return
 	}
 
 	// Handle the event before we return.
 	if err := hook.handle(event); err != nil {
 		writer.WriteHeader(500)
-		writer.Write([]byte(fmt.Sprintf("error handling the event: %v", err)))
+		fmt.Fprintf(writer, "error handling the event: %v", err)
 		return
 	}
 
diff --git a/gitlab.go b/gitlab.go
index 234f58a..9d9892d 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -116,6 +116,7 @@ type Client struct {
 	CustomAttribute         *CustomAttributesService
 	DeployKeys              *DeployKeysService
 	DeployTokens            *DeployTokensService
+	DeploymentMergeRequests *DeploymentMergeRequestsService
 	Deployments             *DeploymentsService
 	Discussions             *DiscussionsService
 	Environments            *EnvironmentsService
@@ -311,13 +312,14 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Boards = &IssueBoardsService{client: c}
 	c.Branches = &BranchesService{client: c}
 	c.BroadcastMessage = &BroadcastMessagesService{client: c}
-	c.ClusterAgents = &ClusterAgentsService{client: c}
 	c.CIYMLTemplate = &CIYMLTemplatesService{client: c}
+	c.ClusterAgents = &ClusterAgentsService{client: c}
 	c.Commits = &CommitsService{client: c}
 	c.ContainerRegistry = &ContainerRegistryService{client: c}
 	c.CustomAttribute = &CustomAttributesService{client: c}
 	c.DeployKeys = &DeployKeysService{client: c}
 	c.DeployTokens = &DeployTokensService{client: c}
+	c.DeploymentMergeRequests = &DeploymentMergeRequestsService{client: c}
 	c.Deployments = &DeploymentsService{client: c}
 	c.Discussions = &DiscussionsService{client: c}
 	c.Environments = &EnvironmentsService{client: c}
@@ -370,8 +372,8 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.PipelineTriggers = &PipelineTriggersService{client: c}
 	c.Pipelines = &PipelinesService{client: c}
 	c.PlanLimits = &PlanLimitsService{client: c}
-	c.ProjectBadges = &ProjectBadgesService{client: c}
 	c.ProjectAccessTokens = &ProjectAccessTokensService{client: c}
+	c.ProjectBadges = &ProjectBadgesService{client: c}
 	c.ProjectCluster = &ProjectClustersService{client: c}
 	c.ProjectImportExport = &ProjectImportExportService{client: c}
 	c.ProjectIterations = &ProjectIterationsService{client: c}


Test patch:
diff --git a/deployments_merge_requests_test.go b/deployments_merge_requests_test.go
new file mode 100644
index 0000000..bb9338c
--- /dev/null
+++ b/deployments_merge_requests_test.go
@@ -0,0 +1,51 @@
+package gitlab
+
+import (
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDeploymentMergeRequestsService_ListDeploymentMergeRequests(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/278964/deployments/2/merge_requests", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		testParams(t, r, "assignee_id=Any&with_labels_details=true&with_merge_status_recheck=true")
+		mustWriteHTTPResponse(t, w, "testdata/get_merge_requests.json")
+	})
+
+	opts := ListMergeRequestsOptions{
+		AssigneeID:             AssigneeID(UserIDAny),
+		WithLabelsDetails:      Bool(true),
+		WithMergeStatusRecheck: Bool(true),
+	}
+
+	mergeRequests, _, err := client.DeploymentMergeRequests.ListDeploymentMergeRequests(278964, 2, &opts)
+	require.NoError(t, err)
+	require.Equal(t, 20, len(mergeRequests))
+
+	validStates := []string{"opened", "closed", "locked", "merged"}
+	mergeStatuses := []string{"can_be_merged", "cannot_be_merged"}
+	allCreatedBefore := time.Date(2019, 8, 21, 0, 0, 0, 0, time.UTC)
+	allCreatedAfter := time.Date(2019, 8, 17, 0, 0, 0, 0, time.UTC)
+
+	for _, mr := range mergeRequests {
+		require.Equal(t, 278964, mr.ProjectID)
+		require.Contains(t, validStates, mr.State)
+		assert.Less(t, mr.CreatedAt.Unix(), allCreatedBefore.Unix())
+		assert.Greater(t, mr.CreatedAt.Unix(), allCreatedAfter.Unix())
+		assert.LessOrEqual(t, mr.CreatedAt.Unix(), mr.UpdatedAt.Unix())
+		assert.LessOrEqual(t, mr.TaskCompletionStatus.CompletedCount, mr.TaskCompletionStatus.Count)
+		require.Contains(t, mergeStatuses, mr.MergeStatus)
+
+		// list requests do not provide these fields:
+		assert.Nil(t, mr.Pipeline)
+		assert.Nil(t, mr.HeadPipeline)
+		assert.Equal(t, "", mr.DiffRefs.HeadSha)
+	}
+}
diff --git a/examples/pipelinestestreport.go b/examples/pipelinestestreport.go
index a97a908..1c7e6c3 100644
--- a/examples/pipelinestestreport.go
+++ b/examples/pipelinestestreport.go
@@ -43,7 +43,7 @@ func pipelineTestReportExample() {
 		if err != nil {
 			log.Fatal(err)
 		}
-		log.Printf("Found test report: %v", report)
 
+		log.Printf("Found test report: %v", report)
 	}
 }