Loading docs/gl_objects/merge_request_approvals.rst +77 −16 Original line number Diff line number Diff line Loading @@ -2,8 +2,47 @@ Merge request approvals settings ################################ Merge request approvals can be defined at the project level or at the merge request level. Merge request approvals can be defined at the group level, or the project level or at the merge request level. Group approval rules ==================== References ---------- * v4 API: + :class:`gitlab.v4.objects.GroupApprovalRule` + :class:`gitlab.v4.objects.GroupApprovalRuleManager` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Examples -------- List group-level MR approval rules:: group_approval_rules = group.approval_rules.list() Change group-level MR approval rule:: g_approval_rule = group.approval_rules.get(123) g_approval_rule.user_ids = [234] g_approval_rule.save() Create new group-level MR approval rule:: group.approval_rules.create({ "name": "my new approval rule", "approvals_required": 2, "rule_type": "regular", "user_ids": [105], "group_ids": [653, 654], }) Project approval rules ====================== References ---------- Loading @@ -15,15 +54,6 @@ References + :class:`gitlab.v4.objects.ProjectApprovalRule` + :class:`gitlab.v4.objects.ProjectApprovalRuleManager` + :attr:`gitlab.v4.objects.Project.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApproval` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRule` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRuleManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_rules` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalState` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalStateManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_state` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Loading @@ -43,7 +73,41 @@ Delete project-level MR approval rule:: p_approvalrule.delete() Get project-level or MR-level MR approvals settings:: Get project-level MR approvals settings:: p_mras = project.approvals.get() Change project-level MR approvals settings:: p_mras.approvals_before_merge = 2 p_mras.save() Merge request approval rules ============================ References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectMergeRequestApproval` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRule` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRuleManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_rules` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalState` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalStateManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_state` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Examples -------- Get MR-level MR approvals settings:: p_mras = project.approvals.get() Loading @@ -53,10 +117,7 @@ Get MR-level approval state:: mr_approval_state = mr.approval_state.get() Change project-level or MR-level MR approvals settings:: p_mras.approvals_before_merge = 2 p_mras.save() Change MR-level MR approvals settings:: mr.approvals.set_approvers(approvals_required=1) # or Loading gitlab/v4/objects/groups.py +2 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ from .members import ( # noqa: F401 GroupMemberAllManager, GroupMemberManager, ) from .merge_request_approvals import GroupApprovalRuleManager from .merge_requests import GroupMergeRequestManager # noqa: F401 from .milestones import GroupMilestoneManager # noqa: F401 from .notification_settings import GroupNotificationSettingsManager # noqa: F401 Loading Loading @@ -70,6 +71,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): access_tokens: GroupAccessTokenManager accessrequests: GroupAccessRequestManager approval_rules: GroupApprovalRuleManager audit_events: GroupAuditEventManager badges: GroupBadgeManager billable_members: GroupBillableMemberManager Loading gitlab/v4/objects/merge_request_approvals.py +22 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ from gitlab.mixins import ( from gitlab.types import RequiredOptional __all__ = [ "GroupApprovalRule", "GroupApprovalRuleManager", "ProjectApproval", "ProjectApprovalManager", "ProjectApprovalRule", Loading @@ -29,6 +31,26 @@ __all__ = [ ] class GroupApprovalRule(SaveMixin, RESTObject): _id_attr = "id" _repr_attr = "name" class GroupApprovalRuleManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager): _path = "/groups/{group_id}/approval_rules" _obj_cls = GroupApprovalRule _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional( required=("name", "approvals_required"), optional=("user_ids", "group_ids", "rule_type"), ) def get( self, id: Union[str, int], lazy: bool = False, **kwargs: Any ) -> GroupApprovalRule: return cast(GroupApprovalRule, super().get(id=id, lazy=lazy, **kwargs)) class ProjectApproval(SaveMixin, RESTObject): _id_attr = None Loading tests/unit/objects/test_group_merge_request_approvals.py 0 → 100644 +253 −0 Original line number Diff line number Diff line """ Gitlab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html """ import copy import json import pytest import responses approval_rule_id = 7 approval_rule_name = "security" approvals_required = 3 user_ids = [5, 50] group_ids = [5] new_approval_rule_name = "new approval rule" new_approval_rule_user_ids = user_ids new_approval_rule_approvals_required = 2 updated_approval_rule_user_ids = [5] updated_approval_rule_approvals_required = 1 @pytest.fixture def resp_group_approval_rules(): content = [ { "id": approval_rule_id, "name": approval_rule_name, "rule_type": "regular", "report_type": None, "eligible_approvers": [ { "id": user_ids[0], "name": "John Doe", "username": "jdoe", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/jdoe", }, { "id": user_ids[1], "name": "Group Member 1", "username": "group_member_1", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/group_member_1", }, ], "approvals_required": approvals_required, "users": [ { "id": 5, "name": "John Doe", "username": "jdoe", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/jdoe", } ], "groups": [ { "id": 5, "name": "group1", "path": "group1", "description": "", "visibility": "public", "lfs_enabled": False, "avatar_url": None, "web_url": "http://localhost/groups/group1", "request_access_enabled": False, "full_name": "group1", "full_path": "group1", "parent_id": None, "ldap_cn": None, "ldap_access": None, } ], "applies_to_all_protected_branches": False, "protected_branches": [ { "id": 1, "name": "main", "push_access_levels": [ { "access_level": 30, "access_level_description": "Developers + Maintainers", } ], "merge_access_levels": [ { "access_level": 30, "access_level_description": "Developers + Maintainers", } ], "unprotect_access_levels": [ {"access_level": 40, "access_level_description": "Maintainers"} ], "code_owner_approval_required": "false", } ], "contains_hidden_groups": False, } ] new_content = dict(content[0]) new_content["id"] = approval_rule_id + 1 # Assign a new ID for the new rule new_content["name"] = new_approval_rule_name new_content["approvals_required"] = new_approval_rule_approvals_required updated_mr_ars_content = copy.deepcopy(content[0]) updated_mr_ars_content["name"] = new_approval_rule_name updated_mr_ars_content["approvals_required"] = ( updated_approval_rule_approvals_required ) list_request_options = { "include_newly_created_rule": False, "updated_first_rule": False, } def list_request_callback(request): if request.method == "GET": if list_request_options["include_newly_created_rule"]: # Include newly created rule in the list response return ( 200, {"Content-Type": "application/json"}, json.dumps(content + [new_content]), ) elif list_request_options["updated_first_rule"]: # Include updated first rule in the list response return ( 200, {"Content-Type": "application/json"}, json.dumps([updated_mr_ars_content]), ) else: return (200, {"Content-Type": "application/json"}, json.dumps(content)) return (404, {}, "") with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: # Mock the API responses for listing all rules for group with ID 1 rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/approval_rules", json=content, content_type="application/json", status=200, ) # Mock the API responses for listing all rules for group with ID 1 # Use a callback to dynamically determine the response based on the request rsps.add_callback( method=responses.GET, url="http://localhost/api/v4/groups/1/approval_rules", callback=list_request_callback, content_type="application/json", ) # Mock the API responses for getting a specific rule for group with ID 1 and approvalrule with ID 7 rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/approval_rules/7", json=content[0], content_type="application/json", status=200, ) # Mock the API responses for creating a new rule for group with ID 1 rsps.add( method=responses.POST, url="http://localhost/api/v4/groups/1/approval_rules", json=new_content, content_type="application/json", status=200, ) # Mock the API responses for updating a specific rule for group with ID 1 and approval rule with ID 7 rsps.add( method=responses.PUT, url="http://localhost/api/v4/groups/1/approval_rules/7", json=updated_mr_ars_content, content_type="application/json", status=200, ) yield rsps, list_request_options def test_list_group_mr_approval_rules(group, resp_group_approval_rules): approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == approval_rule_name assert approval_rules[0].id == approval_rule_id assert ( repr(approval_rules[0]) == f"<GroupApprovalRule id:{approval_rule_id} name:{approval_rule_name}>" ) def test_save_group_mr_approval_rule(group, resp_group_approval_rules): _, list_request_options = resp_group_approval_rules # Before: existing approval rule approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == approval_rule_name rule_to_be_changed = group.approval_rules.get(approval_rules[0].id) rule_to_be_changed.name = new_approval_rule_name rule_to_be_changed.approvals_required = new_approval_rule_approvals_required rule_to_be_changed.save() # Set the flag to return updated rule in the list response list_request_options["updated_first_rule"] = True # After: changed approval rule approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == new_approval_rule_name assert ( repr(approval_rules[0]) == f"<GroupApprovalRule id:{approval_rule_id} name:{new_approval_rule_name}>" ) def test_create_group_mr_approval_rule(group, resp_group_approval_rules): _, list_request_options = resp_group_approval_rules # Before: existing approval rules approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 new_approval_rule_data = { "name": new_approval_rule_name, "approvals_required": new_approval_rule_approvals_required, "rule_type": "regular", "user_ids": new_approval_rule_user_ids, "group_ids": group_ids, } response = group.approval_rules.create(new_approval_rule_data) assert response.approvals_required == new_approval_rule_approvals_required assert len(response.eligible_approvers) == len(new_approval_rule_user_ids) assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0] assert response.name == new_approval_rule_name # Set the flag to include the new rule in the list response list_request_options["include_newly_created_rule"] = True # After: list approval rules approval_rules = group.approval_rules.list() assert len(approval_rules) == 2 assert approval_rules[1].name == new_approval_rule_name assert approval_rules[1].approvals_required == new_approval_rule_approvals_required Loading
docs/gl_objects/merge_request_approvals.rst +77 −16 Original line number Diff line number Diff line Loading @@ -2,8 +2,47 @@ Merge request approvals settings ################################ Merge request approvals can be defined at the project level or at the merge request level. Merge request approvals can be defined at the group level, or the project level or at the merge request level. Group approval rules ==================== References ---------- * v4 API: + :class:`gitlab.v4.objects.GroupApprovalRule` + :class:`gitlab.v4.objects.GroupApprovalRuleManager` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Examples -------- List group-level MR approval rules:: group_approval_rules = group.approval_rules.list() Change group-level MR approval rule:: g_approval_rule = group.approval_rules.get(123) g_approval_rule.user_ids = [234] g_approval_rule.save() Create new group-level MR approval rule:: group.approval_rules.create({ "name": "my new approval rule", "approvals_required": 2, "rule_type": "regular", "user_ids": [105], "group_ids": [653, 654], }) Project approval rules ====================== References ---------- Loading @@ -15,15 +54,6 @@ References + :class:`gitlab.v4.objects.ProjectApprovalRule` + :class:`gitlab.v4.objects.ProjectApprovalRuleManager` + :attr:`gitlab.v4.objects.Project.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApproval` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRule` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRuleManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_rules` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalState` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalStateManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_state` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Loading @@ -43,7 +73,41 @@ Delete project-level MR approval rule:: p_approvalrule.delete() Get project-level or MR-level MR approvals settings:: Get project-level MR approvals settings:: p_mras = project.approvals.get() Change project-level MR approvals settings:: p_mras.approvals_before_merge = 2 p_mras.save() Merge request approval rules ============================ References ---------- * v4 API: + :class:`gitlab.v4.objects.ProjectMergeRequestApproval` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approvals` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRule` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalRuleManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_rules` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalState` + :class:`gitlab.v4.objects.ProjectMergeRequestApprovalStateManager` + :attr:`gitlab.v4.objects.ProjectMergeRequest.approval_state` * GitLab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html Examples -------- Get MR-level MR approvals settings:: p_mras = project.approvals.get() Loading @@ -53,10 +117,7 @@ Get MR-level approval state:: mr_approval_state = mr.approval_state.get() Change project-level or MR-level MR approvals settings:: p_mras.approvals_before_merge = 2 p_mras.save() Change MR-level MR approvals settings:: mr.approvals.set_approvers(approvals_required=1) # or Loading
gitlab/v4/objects/groups.py +2 −0 Original line number Diff line number Diff line Loading @@ -39,6 +39,7 @@ from .members import ( # noqa: F401 GroupMemberAllManager, GroupMemberManager, ) from .merge_request_approvals import GroupApprovalRuleManager from .merge_requests import GroupMergeRequestManager # noqa: F401 from .milestones import GroupMilestoneManager # noqa: F401 from .notification_settings import GroupNotificationSettingsManager # noqa: F401 Loading Loading @@ -70,6 +71,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): access_tokens: GroupAccessTokenManager accessrequests: GroupAccessRequestManager approval_rules: GroupApprovalRuleManager audit_events: GroupAuditEventManager badges: GroupBadgeManager billable_members: GroupBillableMemberManager Loading
gitlab/v4/objects/merge_request_approvals.py +22 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ from gitlab.mixins import ( from gitlab.types import RequiredOptional __all__ = [ "GroupApprovalRule", "GroupApprovalRuleManager", "ProjectApproval", "ProjectApprovalManager", "ProjectApprovalRule", Loading @@ -29,6 +31,26 @@ __all__ = [ ] class GroupApprovalRule(SaveMixin, RESTObject): _id_attr = "id" _repr_attr = "name" class GroupApprovalRuleManager(RetrieveMixin, CreateMixin, UpdateMixin, RESTManager): _path = "/groups/{group_id}/approval_rules" _obj_cls = GroupApprovalRule _from_parent_attrs = {"group_id": "id"} _create_attrs = RequiredOptional( required=("name", "approvals_required"), optional=("user_ids", "group_ids", "rule_type"), ) def get( self, id: Union[str, int], lazy: bool = False, **kwargs: Any ) -> GroupApprovalRule: return cast(GroupApprovalRule, super().get(id=id, lazy=lazy, **kwargs)) class ProjectApproval(SaveMixin, RESTObject): _id_attr = None Loading
tests/unit/objects/test_group_merge_request_approvals.py 0 → 100644 +253 −0 Original line number Diff line number Diff line """ Gitlab API: https://docs.gitlab.com/ee/api/merge_request_approvals.html """ import copy import json import pytest import responses approval_rule_id = 7 approval_rule_name = "security" approvals_required = 3 user_ids = [5, 50] group_ids = [5] new_approval_rule_name = "new approval rule" new_approval_rule_user_ids = user_ids new_approval_rule_approvals_required = 2 updated_approval_rule_user_ids = [5] updated_approval_rule_approvals_required = 1 @pytest.fixture def resp_group_approval_rules(): content = [ { "id": approval_rule_id, "name": approval_rule_name, "rule_type": "regular", "report_type": None, "eligible_approvers": [ { "id": user_ids[0], "name": "John Doe", "username": "jdoe", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/jdoe", }, { "id": user_ids[1], "name": "Group Member 1", "username": "group_member_1", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/group_member_1", }, ], "approvals_required": approvals_required, "users": [ { "id": 5, "name": "John Doe", "username": "jdoe", "state": "active", "avatar_url": "https://www.gravatar.com/avatar/0?s=80&d=identicon", "web_url": "http://localhost/jdoe", } ], "groups": [ { "id": 5, "name": "group1", "path": "group1", "description": "", "visibility": "public", "lfs_enabled": False, "avatar_url": None, "web_url": "http://localhost/groups/group1", "request_access_enabled": False, "full_name": "group1", "full_path": "group1", "parent_id": None, "ldap_cn": None, "ldap_access": None, } ], "applies_to_all_protected_branches": False, "protected_branches": [ { "id": 1, "name": "main", "push_access_levels": [ { "access_level": 30, "access_level_description": "Developers + Maintainers", } ], "merge_access_levels": [ { "access_level": 30, "access_level_description": "Developers + Maintainers", } ], "unprotect_access_levels": [ {"access_level": 40, "access_level_description": "Maintainers"} ], "code_owner_approval_required": "false", } ], "contains_hidden_groups": False, } ] new_content = dict(content[0]) new_content["id"] = approval_rule_id + 1 # Assign a new ID for the new rule new_content["name"] = new_approval_rule_name new_content["approvals_required"] = new_approval_rule_approvals_required updated_mr_ars_content = copy.deepcopy(content[0]) updated_mr_ars_content["name"] = new_approval_rule_name updated_mr_ars_content["approvals_required"] = ( updated_approval_rule_approvals_required ) list_request_options = { "include_newly_created_rule": False, "updated_first_rule": False, } def list_request_callback(request): if request.method == "GET": if list_request_options["include_newly_created_rule"]: # Include newly created rule in the list response return ( 200, {"Content-Type": "application/json"}, json.dumps(content + [new_content]), ) elif list_request_options["updated_first_rule"]: # Include updated first rule in the list response return ( 200, {"Content-Type": "application/json"}, json.dumps([updated_mr_ars_content]), ) else: return (200, {"Content-Type": "application/json"}, json.dumps(content)) return (404, {}, "") with responses.RequestsMock(assert_all_requests_are_fired=False) as rsps: # Mock the API responses for listing all rules for group with ID 1 rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/approval_rules", json=content, content_type="application/json", status=200, ) # Mock the API responses for listing all rules for group with ID 1 # Use a callback to dynamically determine the response based on the request rsps.add_callback( method=responses.GET, url="http://localhost/api/v4/groups/1/approval_rules", callback=list_request_callback, content_type="application/json", ) # Mock the API responses for getting a specific rule for group with ID 1 and approvalrule with ID 7 rsps.add( method=responses.GET, url="http://localhost/api/v4/groups/1/approval_rules/7", json=content[0], content_type="application/json", status=200, ) # Mock the API responses for creating a new rule for group with ID 1 rsps.add( method=responses.POST, url="http://localhost/api/v4/groups/1/approval_rules", json=new_content, content_type="application/json", status=200, ) # Mock the API responses for updating a specific rule for group with ID 1 and approval rule with ID 7 rsps.add( method=responses.PUT, url="http://localhost/api/v4/groups/1/approval_rules/7", json=updated_mr_ars_content, content_type="application/json", status=200, ) yield rsps, list_request_options def test_list_group_mr_approval_rules(group, resp_group_approval_rules): approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == approval_rule_name assert approval_rules[0].id == approval_rule_id assert ( repr(approval_rules[0]) == f"<GroupApprovalRule id:{approval_rule_id} name:{approval_rule_name}>" ) def test_save_group_mr_approval_rule(group, resp_group_approval_rules): _, list_request_options = resp_group_approval_rules # Before: existing approval rule approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == approval_rule_name rule_to_be_changed = group.approval_rules.get(approval_rules[0].id) rule_to_be_changed.name = new_approval_rule_name rule_to_be_changed.approvals_required = new_approval_rule_approvals_required rule_to_be_changed.save() # Set the flag to return updated rule in the list response list_request_options["updated_first_rule"] = True # After: changed approval rule approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 assert approval_rules[0].name == new_approval_rule_name assert ( repr(approval_rules[0]) == f"<GroupApprovalRule id:{approval_rule_id} name:{new_approval_rule_name}>" ) def test_create_group_mr_approval_rule(group, resp_group_approval_rules): _, list_request_options = resp_group_approval_rules # Before: existing approval rules approval_rules = group.approval_rules.list() assert len(approval_rules) == 1 new_approval_rule_data = { "name": new_approval_rule_name, "approvals_required": new_approval_rule_approvals_required, "rule_type": "regular", "user_ids": new_approval_rule_user_ids, "group_ids": group_ids, } response = group.approval_rules.create(new_approval_rule_data) assert response.approvals_required == new_approval_rule_approvals_required assert len(response.eligible_approvers) == len(new_approval_rule_user_ids) assert response.eligible_approvers[0]["id"] == new_approval_rule_user_ids[0] assert response.name == new_approval_rule_name # Set the flag to include the new rule in the list response list_request_options["include_newly_created_rule"] = True # After: list approval rules approval_rules = group.approval_rules.list() assert len(approval_rules) == 2 assert approval_rules[1].name == new_approval_rule_name assert approval_rules[1].approvals_required == new_approval_rule_approvals_required