Commit c7ffe6ff authored by Prakash Divy's avatar Prakash Divy Committed by Heidi Berry
Browse files

feat: add group protected branches service

Changelog: Improvements
parent ca0d7b7b
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -198,6 +198,7 @@ type Client struct {
	GroupMembers                     GroupMembersServiceInterface
	GroupMilestones                  GroupMilestonesServiceInterface
	GroupProtectedEnvironments       GroupProtectedEnvironmentsServiceInterface
	GroupProtectedBranches           GroupProtectedBranchesServiceInterface
	GroupRelationsExport             GroupRelationsExportServiceInterface
	GroupReleases                    GroupReleasesServiceInterface
	GroupRepositoryStorageMove       GroupRepositoryStorageMoveServiceInterface
@@ -517,6 +518,7 @@ func NewAuthSourceClient(as AuthSource, options ...ClientOptionFunc) (*Client, e
	c.GroupMembers = &GroupMembersService{client: c}
	c.GroupMilestones = &GroupMilestonesService{client: c}
	c.GroupProtectedEnvironments = &GroupProtectedEnvironmentsService{client: c}
	c.GroupProtectedBranches = &GroupProtectedBranchesService{client: c}
	c.GroupRelationsExport = &GroupRelationsExportService{client: c}
	c.GroupReleases = &GroupReleasesService{client: c}
	c.GroupRepositoryStorageMove = &GroupRepositoryStorageMoveService{client: c}
+1 −0
Original line number Diff line number Diff line
@@ -61,6 +61,7 @@ var serviceMap = map[any]any{
	&GroupMarkdownUploadsService{}:             (*GroupMarkdownUploadsServiceInterface)(nil),
	&GroupMembersService{}:                     (*GroupMembersServiceInterface)(nil),
	&GroupMilestonesService{}:                  (*GroupMilestonesServiceInterface)(nil),
	&GroupProtectedBranchesService{}:           (*GroupProtectedBranchesServiceInterface)(nil),
	&GroupProtectedEnvironmentsService{}:       (*GroupProtectedEnvironmentsServiceInterface)(nil),
	&GroupRelationsExportService{}:             (*GroupRelationsExportServiceInterface)(nil),
	&GroupReleasesService{}:                    (*GroupReleasesServiceInterface)(nil),
+59 −0
Original line number Diff line number Diff line
@@ -107,3 +107,62 @@ func Test_GroupsMaxArtifactsSize_Integration(t *testing.T) {
	// THEN MaxArtifactsSize should persist
	assert.Equal(t, int64(100), retrievedGroup.MaxArtifactsSize)
}

func Test_GroupProtectedBranches_Integration(t *testing.T) {
	// GIVEN a GitLab client and a test group
	client := SetupIntegrationClient(t)
	group := CreateTestGroup(t, client)

	// Define branch name
	branchName := "main"

	// WHEN protecting a branch
	protectedBranch, _, err := client.GroupProtectedBranches.ProtectRepositoryBranches(group.ID, &gitlab.ProtectGroupRepositoryBranchesOptions{
		Name:             gitlab.Ptr(branchName),
		PushAccessLevel:  gitlab.Ptr(gitlab.MaintainerPermissions),
		MergeAccessLevel: gitlab.Ptr(gitlab.MaintainerPermissions),
	})
	require.NoError(t, err, "Failed to protect branch")

	// THEN the branch should be protected
	assert.Equal(t, branchName, protectedBranch.Name)

	// WHEN listing protected branches
	branches, _, err := client.GroupProtectedBranches.ListProtectedBranches(group.ID, nil)
	require.NoError(t, err, "Failed to list protected branches")

	// THEN the protected branch should be in the list
	found := false
	for _, b := range branches {
		if b.Name == branchName {
			found = true
			break
		}
	}
	assert.True(t, found, "Protected branch not found in list")

	// WHEN getting the protected branch
	gotBranch, _, err := client.GroupProtectedBranches.GetProtectedBranch(group.ID, branchName)
	require.NoError(t, err, "Failed to get protected branch")

	// THEN it should match
	assert.Equal(t, branchName, gotBranch.Name)

	// WHEN updating the protected branch
	updatedBranch, _, err := client.GroupProtectedBranches.UpdateProtectedBranch(group.ID, branchName, &gitlab.UpdateGroupProtectedBranchOptions{
		AllowForcePush: gitlab.Ptr(true),
	})
	require.NoError(t, err, "Failed to update protected branch")

	// THEN the update should be reflected
	assert.True(t, updatedBranch.AllowForcePush)

	// WHEN unprotecting the branch
	_, err = client.GroupProtectedBranches.UnprotectRepositoryBranches(group.ID, branchName)
	require.NoError(t, err, "Failed to unprotect branch")

	// THEN getting the branch should fail (404)
	_, resp, err := client.GroupProtectedBranches.GetProtectedBranch(group.ID, branchName)
	assert.Error(t, err)
	assert.Equal(t, 404, resp.StatusCode)
}
+177 −0
Original line number Diff line number Diff line
package gitlab

import (
	"net/http"
	"net/url"
)

type (
	GroupProtectedBranchesServiceInterface interface {
		// ListProtectedBranches returns a list of protected branches from a group.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/group_protected_branches/#list-protected-branches
		ListProtectedBranches(gid any, opt *ListGroupProtectedBranchesOptions, options ...RequestOptionFunc) ([]*GroupProtectedBranch, *Response, error)

		// GetProtectedBranch returns a single group-level protected branch.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/group_protected_branches/#get-a-single-protected-branch-or-wildcard-protected-branch
		GetProtectedBranch(gid any, branch string, options ...RequestOptionFunc) (*GroupProtectedBranch, *Response, error)

		// ProtectRepositoryBranches protects a single group-level branch.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/group_protected_branches/#protect-repository-branches
		ProtectRepositoryBranches(gid any, opt *ProtectGroupRepositoryBranchesOptions, options ...RequestOptionFunc) (*GroupProtectedBranch, *Response, error)

		// UpdateProtectedBranch updates a single group-level protected branch.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/group_protected_branches/#update-a-protected-branch
		UpdateProtectedBranch(gid any, branch string, opt *UpdateGroupProtectedBranchOptions, options ...RequestOptionFunc) (*GroupProtectedBranch, *Response, error)

		// UnprotectRepositoryBranches unprotects the given protected group-level branch.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/group_protected_branches/#unprotect-repository-branches
		UnprotectRepositoryBranches(gid any, branch string, options ...RequestOptionFunc) (*Response, error)
	}

	// GroupProtectedBranchesService handles communication with the group-level
	// protected branch methods of the GitLab API.
	//
	// GitLab API docs:
	// https://docs.gitlab.com/api/group_protected_branches/
	GroupProtectedBranchesService struct {
		client *Client
	}
)

var _ GroupProtectedBranchesServiceInterface = (*GroupProtectedBranchesService)(nil)

// GroupProtectedBranch represents a group protected branch.
//
// GitLab API docs:
// https://docs.gitlab.com/api/group_protected_branches/#list-protected-branches
type GroupProtectedBranch struct {
	ID                        int64                           `json:"id"`
	Name                      string                          `json:"name"`
	PushAccessLevels          []*GroupBranchAccessDescription `json:"push_access_levels"`
	MergeAccessLevels         []*GroupBranchAccessDescription `json:"merge_access_levels"`
	UnprotectAccessLevels     []*GroupBranchAccessDescription `json:"unprotect_access_levels"`
	AllowForcePush            bool                            `json:"allow_force_push"`
	CodeOwnerApprovalRequired bool                            `json:"code_owner_approval_required"`
}

// GroupBranchAccessDescription represents the access description for a group protected
// branch.
//
// GitLab API docs:
// https://docs.gitlab.com/api/group_protected_branches/#list-protected-branches
type GroupBranchAccessDescription struct {
	ID                     int64            `json:"id"`
	AccessLevel            AccessLevelValue `json:"access_level"`
	AccessLevelDescription string           `json:"access_level_description"`
	DeployKeyID            int64            `json:"deploy_key_id"`
	UserID                 int64            `json:"user_id"`
	GroupID                int64            `json:"group_id"`
}

// ListGroupProtectedBranchesOptions represents the available ListProtectedBranches()
// options.
//
// GitLab API docs:
// https://docs.gitlab.com/api/group_protected_branches/#list-protected-branches
type ListGroupProtectedBranchesOptions struct {
	ListOptions
	Search *string `url:"search,omitempty" json:"search,omitempty"`
}

func (s *GroupProtectedBranchesService) ListProtectedBranches(gid any, opt *ListGroupProtectedBranchesOptions, options ...RequestOptionFunc) ([]*GroupProtectedBranch, *Response, error) {
	return do[[]*GroupProtectedBranch](s.client,
		withMethod(http.MethodGet),
		withPath("groups/%s/protected_branches", GroupID{gid}),
		withAPIOpts(opt),
		withRequestOpts(options...),
	)
}

func (s *GroupProtectedBranchesService) GetProtectedBranch(gid any, branch string, options ...RequestOptionFunc) (*GroupProtectedBranch, *Response, error) {
	return do[*GroupProtectedBranch](s.client,
		withMethod(http.MethodGet),
		withPath("groups/%s/protected_branches/%s", GroupID{gid}, url.PathEscape(branch)),
		withRequestOpts(options...),
	)
}

// ProtectGroupRepositoryBranchesOptions represents the available
// ProtectRepositoryBranches() options.
//
// GitLab API docs:
// https://docs.gitlab.com/api/group_protected_branches/#protect-repository-branches
type ProtectGroupRepositoryBranchesOptions struct {
	Name                      *string                          `url:"name,omitempty" json:"name,omitempty"`
	PushAccessLevel           *AccessLevelValue                `url:"push_access_level,omitempty" json:"push_access_level,omitempty"`
	MergeAccessLevel          *AccessLevelValue                `url:"merge_access_level,omitempty" json:"merge_access_level,omitempty"`
	UnprotectAccessLevel      *AccessLevelValue                `url:"unprotect_access_level,omitempty" json:"unprotect_access_level,omitempty"`
	AllowForcePush            *bool                            `url:"allow_force_push,omitempty" json:"allow_force_push,omitempty"`
	AllowedToPush             *[]*GroupBranchPermissionOptions `url:"allowed_to_push,omitempty" json:"allowed_to_push,omitempty"`
	AllowedToMerge            *[]*GroupBranchPermissionOptions `url:"allowed_to_merge,omitempty" json:"allowed_to_merge,omitempty"`
	AllowedToUnprotect        *[]*GroupBranchPermissionOptions `url:"allowed_to_unprotect,omitempty" json:"allowed_to_unprotect,omitempty"`
	CodeOwnerApprovalRequired *bool                            `url:"code_owner_approval_required,omitempty" json:"code_owner_approval_required,omitempty"`
}

// GroupBranchPermissionOptions represents a branch permission option.
//
// GitLab API docs:
// https://docs.gitlab.com/api/group_protected_branches/#protect-repository-branches
type GroupBranchPermissionOptions struct {
	ID          *int64            `url:"id,omitempty" json:"id,omitempty"`
	UserID      *int64            `url:"user_id,omitempty" json:"user_id,omitempty"`
	GroupID     *int64            `url:"group_id,omitempty" json:"group_id,omitempty"`
	DeployKeyID *int64            `url:"deploy_key_id,omitempty" json:"deploy_key_id,omitempty"`
	AccessLevel *AccessLevelValue `url:"access_level,omitempty" json:"access_level,omitempty"`
	Destroy     *bool             `url:"_destroy,omitempty" json:"_destroy,omitempty"`
}

func (s *GroupProtectedBranchesService) ProtectRepositoryBranches(gid any, opt *ProtectGroupRepositoryBranchesOptions, options ...RequestOptionFunc) (*GroupProtectedBranch, *Response, error) {
	return do[*GroupProtectedBranch](s.client,
		withMethod(http.MethodPost),
		withPath("groups/%s/protected_branches", GroupID{gid}),
		withAPIOpts(opt),
		withRequestOpts(options...),
	)
}

// UpdateGroupProtectedBranchOptions represents the available
// UpdateProtectedBranch() options.
//
// GitLab API docs:
// https://docs.gitlab.com/api/group_protected_branches/#update-a-protected-branch
type UpdateGroupProtectedBranchOptions struct {
	Name                      *string                          `url:"name,omitempty" json:"name,omitempty"`
	AllowForcePush            *bool                            `url:"allow_force_push,omitempty" json:"allow_force_push,omitempty"`
	CodeOwnerApprovalRequired *bool                            `url:"code_owner_approval_required,omitempty" json:"code_owner_approval_required,omitempty"`
	AllowedToPush             *[]*GroupBranchPermissionOptions `url:"allowed_to_push,omitempty" json:"allowed_to_push,omitempty"`
	AllowedToMerge            *[]*GroupBranchPermissionOptions `url:"allowed_to_merge,omitempty" json:"allowed_to_merge,omitempty"`
	AllowedToUnprotect        *[]*GroupBranchPermissionOptions `url:"allowed_to_unprotect,omitempty" json:"allowed_to_unprotect,omitempty"`
}

func (s *GroupProtectedBranchesService) UpdateProtectedBranch(gid any, branch string, opt *UpdateGroupProtectedBranchOptions, options ...RequestOptionFunc) (*GroupProtectedBranch, *Response, error) {
	return do[*GroupProtectedBranch](s.client,
		withMethod(http.MethodPatch),
		withPath("groups/%s/protected_branches/%s", GroupID{gid}, url.PathEscape(branch)),
		withAPIOpts(opt),
		withRequestOpts(options...),
	)
}

func (s *GroupProtectedBranchesService) UnprotectRepositoryBranches(gid any, branch string, options ...RequestOptionFunc) (*Response, error) {
	_, resp, err := do[none](s.client,
		withMethod(http.MethodDelete),
		withPath("groups/%s/protected_branches/%s", GroupID{gid}, url.PathEscape(branch)),
		withRequestOpts(options...),
	)
	return resp, err
}
+223 −0
Original line number Diff line number Diff line
package gitlab

import (
	"fmt"
	"net/http"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestGroupListProtectedBranches(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/protected_branches", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `[
	{
		"id":1,
		"name":"master",
		"push_access_levels":[{
			"id":1,
			"access_level":40,
			"access_level_description":"Maintainers",
			"user_id":null,
			"group_id":null
		},{
			"id":2,
			"access_level":30,
			"access_level_description":"User name",
			"user_id":123,
			"group_id":null
		}],
		"merge_access_levels":[{
			"id":1,
			"access_level":40,
			"access_level_description":"Maintainers",
			"user_id":null,
			"group_id":null
		}],
		"code_owner_approval_required":false
	}
]`)
	})
	opt := &ListGroupProtectedBranchesOptions{}
	protectedBranches, resp, err := client.GroupProtectedBranches.ListProtectedBranches("1", opt)
	assert.NoError(t, err)
	assert.NotNil(t, resp)
	want := []*GroupProtectedBranch{
		{
			ID:   1,
			Name: "master",
			PushAccessLevels: []*GroupBranchAccessDescription{
				{
					ID:                     1,
					AccessLevel:            40,
					AccessLevelDescription: "Maintainers",
				},
				{
					ID:                     2,
					AccessLevel:            30,
					AccessLevelDescription: "User name",
					UserID:                 123,
				},
			},
			MergeAccessLevels: []*GroupBranchAccessDescription{
				{
					ID:                     1,
					AccessLevel:            40,
					AccessLevelDescription: "Maintainers",
				},
			},
			AllowForcePush:            false,
			CodeOwnerApprovalRequired: false,
		},
	}
	assert.Equal(t, want, protectedBranches)
}

func TestGroupGetProtectedBranch(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/protected_branches/main", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodGet)
		fmt.Fprint(w, `{
			"id":1,
			"name":"main",
			"push_access_levels":[{
				"id":1,
				"access_level":40,
				"access_level_description":"Maintainers",
				"user_id":null,
				"group_id":null
			}],
			"merge_access_levels":[{
				"id":1,
				"access_level":40,
				"access_level_description":"Maintainers",
				"user_id":null,
				"group_id":null
			}],
			"code_owner_approval_required":false
		}`)
	})
	protectedBranch, resp, err := client.GroupProtectedBranches.GetProtectedBranch(1, "main")
	assert.NoError(t, err)
	assert.NotNil(t, resp)
	want := &GroupProtectedBranch{
		ID:   1,
		Name: "main",
		PushAccessLevels: []*GroupBranchAccessDescription{
			{
				ID:                     1,
				AccessLevel:            40,
				AccessLevelDescription: "Maintainers",
			},
		},
		MergeAccessLevels: []*GroupBranchAccessDescription{
			{
				ID:                     1,
				AccessLevel:            40,
				AccessLevelDescription: "Maintainers",
			},
		},
		AllowForcePush:            false,
		CodeOwnerApprovalRequired: false,
	}
	assert.Equal(t, want, protectedBranch)
}

func TestGroupProtectRepositoryBranches(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/protected_branches", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPost)
		fmt.Fprint(w, `
	{
		"id":1,
		"name":"master",
		"push_access_levels":[{
			"access_level":40,
			"access_level_description":"Maintainers"
		}],
		"merge_access_levels":[{
			"access_level":40,
			"access_level_description":"Maintainers"
		}],
		"allow_force_push":true,
		"code_owner_approval_required":true
	}`)
	})
	opt := &ProtectGroupRepositoryBranchesOptions{
		Name:                      Ptr("master"),
		PushAccessLevel:           Ptr(MaintainerPermissions),
		MergeAccessLevel:          Ptr(MaintainerPermissions),
		AllowForcePush:            Ptr(true),
		CodeOwnerApprovalRequired: Ptr(true),
	}
	protectedBranches, resp, err := client.GroupProtectedBranches.ProtectRepositoryBranches("1", opt)
	assert.NoError(t, err)
	assert.NotNil(t, resp)
	want := &GroupProtectedBranch{
		ID:   1,
		Name: "master",
		PushAccessLevels: []*GroupBranchAccessDescription{
			{
				AccessLevel:            40,
				AccessLevelDescription: "Maintainers",
			},
		},
		MergeAccessLevels: []*GroupBranchAccessDescription{
			{
				AccessLevel:            40,
				AccessLevelDescription: "Maintainers",
			},
		},
		AllowForcePush:            true,
		CodeOwnerApprovalRequired: true,
	}
	assert.Equal(t, want, protectedBranches)
}

func TestGroupUnprotectRepositoryBranches(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/protected_branches/main", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodDelete)
	})
	resp, err := client.GroupProtectedBranches.UnprotectRepositoryBranches("1", "main")
	assert.NoError(t, err)
	assert.NotNil(t, resp)
}

func TestGroupUpdateProtectedBranch(t *testing.T) {
	t.Parallel()
	mux, client := setup(t)

	mux.HandleFunc("/api/v4/groups/1/protected_branches/master", func(w http.ResponseWriter, r *http.Request) {
		testMethod(t, r, http.MethodPatch)
		testBodyJSON(t, r, map[string]bool{
			"code_owner_approval_required": true,
		})
		fmt.Fprintf(w, `{
			"name": "master",
			"code_owner_approval_required": true
		}`)
	})
	opt := &UpdateGroupProtectedBranchOptions{
		CodeOwnerApprovalRequired: Ptr(true),
	}
	protectedBranch, resp, err := client.GroupProtectedBranches.UpdateProtectedBranch("1", "master", opt)
	assert.NoError(t, err)
	assert.NotNil(t, resp)

	want := &GroupProtectedBranch{
		Name:                      "master",
		CodeOwnerApprovalRequired: true,
	}
	assert.Equal(t, want, protectedBranch)
}
Loading