diff --git a/docs/gl_objects/groups.rst b/docs/gl_objects/groups.rst index c4d9fdc1e..b9b865a97 100644 --- a/docs/gl_objects/groups.rst +++ b/docs/gl_objects/groups.rst @@ -376,3 +376,37 @@ Delete a group hook:: group.hooks.delete(hook_id) # or hook.delete() + +Group push rules +================== + +Reference +--------- + +* v4 API: + + + :class:`gitlab.v4.objects.GroupPushRules` + + :class:`gitlab.v4.objects.GroupPushRulesManager` + + :attr:`gitlab.v4.objects.Group.pushrules` + +* GitLab API: https://docs.gitlab.com/ee/api/groups.html#push-rules + +Examples +--------- + +Create group push rules (at least one rule is necessary):: + + group.pushrules.create({'deny_delete_tag': True}) + +Get group push rules (returns None is there are no push rules):: + + pr = group.pushrules.get() + +Edit group push rules:: + + pr.branch_name_regex = '^(master|develop|support-\d+|release-\d+\..+|hotfix-.+|feature-.+)$' + pr.save() + +Delete group push rules:: + + pr.delete() diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py index 8cb505277..84fd6002e 100644 --- a/gitlab/v4/objects/groups.py +++ b/gitlab/v4/objects/groups.py @@ -33,6 +33,7 @@ from .notification_settings import GroupNotificationSettingsManager # noqa: F401 from .packages import GroupPackageManager # noqa: F401 from .projects import GroupProjectManager # noqa: F401 +from .push_rules import GroupPushRulesManager from .runners import GroupRunnerManager # noqa: F401 from .statistics import GroupIssuesStatisticsManager # noqa: F401 from .variables import GroupVariableManager # noqa: F401 @@ -75,6 +76,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): notificationsettings: GroupNotificationSettingsManager packages: GroupPackageManager projects: GroupProjectManager + pushrules: GroupPushRulesManager runners: GroupRunnerManager subgroups: "GroupSubgroupManager" variables: GroupVariableManager diff --git a/gitlab/v4/objects/push_rules.py b/gitlab/v4/objects/push_rules.py index d1fe81c17..9b4980b16 100644 --- a/gitlab/v4/objects/push_rules.py +++ b/gitlab/v4/objects/push_rules.py @@ -12,6 +12,8 @@ from gitlab.types import RequiredOptional __all__ = [ + "GroupPushRules", + "GroupPushRulesManager", "ProjectPushRules", "ProjectPushRulesManager", ] @@ -27,16 +29,64 @@ class ProjectPushRulesManager( _path = "/projects/{project_id}/push_rule" _obj_cls = ProjectPushRules _from_parent_attrs = {"project_id": "id"} + _create_attrs = RequiredOptional( + optional=( + "author_email_regex", + "branch_name_regex", + "commit_committer_check", + "commit_message_negative_regex", + "commit_message_regex", + "deny_delete_tag", + "file_name_regex", + "max_file_size", + "member_check", + "prevent_secrets", + "reject_unsigned_commits", + ), + ) + _update_attrs = RequiredOptional( + optional=( + "author_email_regex", + "branch_name_regex", + "commit_committer_check", + "commit_message_negative_regex", + "commit_message_regex", + "deny_delete_tag", + "file_name_regex", + "max_file_size", + "member_check", + "prevent_secrets", + "reject_unsigned_commits", + ), + ) + + def get(self, **kwargs: Any) -> ProjectPushRules: + return cast(ProjectPushRules, super().get(**kwargs)) + + +class GroupPushRules(SaveMixin, ObjectDeleteMixin, RESTObject): + _id_attr = None + + +class GroupPushRulesManager( + GetWithoutIdMixin, CreateMixin, UpdateMixin, DeleteMixin, RESTManager +): + _path = "/groups/{group_id}/push_rule" + _obj_cls = GroupPushRules + _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional( optional=( "deny_delete_tag", "member_check", "prevent_secrets", "commit_message_regex", + "commit_message_negative_regex", "branch_name_regex", "author_email_regex", "file_name_regex", "max_file_size", + "commit_committer_check", + "reject_unsigned_commits", ), ) _update_attrs = RequiredOptional( @@ -45,12 +95,15 @@ class ProjectPushRulesManager( "member_check", "prevent_secrets", "commit_message_regex", + "commit_message_negative_regex", "branch_name_regex", "author_email_regex", "file_name_regex", "max_file_size", + "commit_committer_check", + "reject_unsigned_commits", ), ) - def get(self, **kwargs: Any) -> ProjectPushRules: - return cast(ProjectPushRules, super().get(**kwargs)) + def get(self, **kwargs: Any) -> GroupPushRules: + return cast(GroupPushRules, super().get(**kwargs)) diff --git a/tests/unit/objects/test_groups.py b/tests/unit/objects/test_groups.py index 2c91d38d8..cebdfc7b0 100644 --- a/tests/unit/objects/test_groups.py +++ b/tests/unit/objects/test_groups.py @@ -38,6 +38,19 @@ "created_at": "2020-01-15T12:36:29.590Z", }, ] +push_rules_content = { + "id": 2, + "created_at": "2020-08-17T19:09:19.580Z", + "commit_message_regex": "[a-zA-Z]", + "commit_message_negative_regex": "[x+]", + "branch_name_regex": "[a-z]", + "deny_delete_tag": True, + "member_check": True, + "prevent_secrets": True, + "author_email_regex": "^[A-Za-z0-9.]+@gitlab.com$", + "file_name_regex": "(exe)$", + "max_file_size": 100, +} @pytest.fixture @@ -111,6 +124,72 @@ def resp_transfer_group(): yield rsps +@pytest.fixture +def resp_list_push_rules_group(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/push_rule", + json=push_rules_content, + content_type="application/json", + status=200, + ) + yield rsps + + +@pytest.fixture +def resp_create_push_rules_group(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.POST, + url="http://localhost/api/v4/groups/1/push_rule", + json=push_rules_content, + content_type="application/json", + status=201, + ) + yield rsps + + +@pytest.fixture +def resp_update_push_rules_group(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/push_rule", + json=push_rules_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.PUT, + url="http://localhost/api/v4/groups/1/push_rule", + json=push_rules_content, + content_type="application/json", + status=201, + ) + yield rsps + + +@pytest.fixture +def resp_delete_push_rules_group(no_content): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/groups/1/push_rule", + json=push_rules_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.DELETE, + url="http://localhost/api/v4/groups/1/push_rule", + json=no_content, + content_type="application/json", + status=204, + ) + yield rsps + + def test_get_group(gl, resp_groups): data = gl.groups.get(1) assert isinstance(data, gitlab.v4.objects.Group) @@ -173,3 +252,27 @@ def test_refresh_group_import_status(group, resp_groups): def test_transfer_group(gl, resp_transfer_group): group = gl.groups.get(1, lazy=True) group.transfer("test-namespace") + + +def test_list_group_push_rules(group, resp_list_push_rules_group): + pr = group.pushrules.get() + assert pr + assert pr.deny_delete_tag + + +def test_create_group_push_rule(group, resp_create_push_rules_group): + group.pushrules.create({"deny_delete_tag": True}) + + +def test_update_group_push_rule( + group, + resp_update_push_rules_group, +): + pr = group.pushrules.get() + pr.deny_delete_tag = False + pr.save() + + +def test_delete_group_push_rule(group, resp_delete_push_rules_group): + pr = group.pushrules.get() + pr.delete()