diff --git a/docs/gl_objects/merge_requests.rst b/docs/gl_objects/merge_requests.rst index 95364073d..8264669e6 100644 --- a/docs/gl_objects/merge_requests.rst +++ b/docs/gl_objects/merge_requests.rst @@ -144,6 +144,10 @@ List the changes of a MR:: changes = mr.changes() +List issues related to this merge request:: + + related_issues = mr.related_issues() + List issues that will close on merge:: mr.closes_issues() diff --git a/gitlab/v4/objects/merge_requests.py b/gitlab/v4/objects/merge_requests.py index fb25ab5ae..e29ab2b28 100644 --- a/gitlab/v4/objects/merge_requests.py +++ b/gitlab/v4/objects/merge_requests.py @@ -197,6 +197,35 @@ def cancel_merge_when_pipeline_succeeds(self, **kwargs: Any) -> Dict[str, str]: assert isinstance(server_data, dict) return server_data + @cli.register_custom_action(cls_names="ProjectMergeRequest") + @exc.on_http_error(exc.GitlabListError) + def related_issues(self, **kwargs: Any) -> RESTObjectList: + """List issues related to this merge request." + + Args: + all: If True, return all the items, without pagination + per_page: Number of items to retrieve per request + page: ID of the page to return (starts with page 1) + **kwargs: Extra options to send to the server (e.g. sudo) + + Raises: + GitlabAuthenticationError: If authentication is not correct + GitlabListError: If the list could not be retrieved + + Returns: + List of issues + """ + + path = f"{self.manager.path}/{self.encoded_id}/related_issues" + data_list = self.manager.gitlab.http_list(path, iterator=True, **kwargs) + + if TYPE_CHECKING: + assert isinstance(data_list, gitlab.GitlabList) + + manager = ProjectIssueManager(self.manager.gitlab, parent=self.manager._parent) + + return RESTObjectList(manager, ProjectIssue, data_list) + @cli.register_custom_action(cls_names="ProjectMergeRequest") @exc.on_http_error(exc.GitlabListError) def closes_issues(self, **kwargs: Any) -> RESTObjectList: diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index 6f8a6a7de..400b24b34 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -9,8 +9,10 @@ import pytest import responses +from gitlab.base import RESTObjectList from gitlab.v4.objects import ( ProjectDeploymentMergeRequest, + ProjectIssue, ProjectMergeRequest, ProjectMergeRequestReviewerDetail, ) @@ -57,6 +59,78 @@ } ] +related_issues = [ + { + "id": 1, + "iid": 1, + "project_id": 1, + "title": "Fake Title for Merge Requests via API", + "description": "Something here", + "state": "closed", + "created_at": "2024-05-14T04:01:40.042Z", + "updated_at": "2024-06-13T05:29:13.661Z", + "closed_at": "2024-06-13T05:29:13.602Z", + "closed_by": { + "id": 2, + "name": "Sam Bauch", + "username": "kenyatta_oconnell", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/956c92487c6f6f7616b536927e22c9a0?s=80&d=identicon", + "web_url": "http://gitlab.example.com/kenyatta_oconnell", + }, + "labels": [ + "FakeCategory", + "fake:ml", + ], + "assignees": [ + { + "id": 2, + "name": "Sam Bauch", + "username": "kenyatta_oconnell", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/956c92487c6f6f7616b536927e22c9a0?s=80&d=identicon", + "web_url": "http://gitlab.example.com/kenyatta_oconnell", + } + ], + "author": { + "id": 2, + "name": "Sam Bauch", + "username": "kenyatta_oconnell", + "state": "active", + "avatar_url": "https://www.gravatar.com/avatar/956c92487c6f6f7616b536927e22c9a0?s=80&d=identicon", + "web_url": "http://gitlab.example.com//kenyatta_oconnell", + }, + "type": "ISSUE", + "assignee": { + "id": 4459593, + "username": "fakeuser", + "name": "Fake User", + "state": "active", + "locked": False, + "avatar_url": "https://example.com/uploads/-/system/user/avatar/4459593/avatar.png", + "web_url": "https://example.com/fakeuser", + }, + "user_notes_count": 9, + "merge_requests_count": 0, + "upvotes": 1, + "downvotes": 0, + "due_date": None, + "confidential": False, + "discussion_locked": None, + "issue_type": "issue", + "web_url": "https://example.com/fakeorg/fakeproject/-/issues/461536", + "time_stats": { + "time_estimate": 0, + "total_time_spent": 0, + "human_time_estimate": None, + "human_total_time_spent": None, + }, + "task_completion_status": {"count": 0, "completed_count": 0}, + "weight": None, + "blocking_issues_count": 0, + } +] + @pytest.fixture def resp_list_merge_requests(): @@ -93,6 +167,26 @@ def resp_get_merge_request_reviewers(): yield rsps +@pytest.fixture +def resp_list_merge_requests_related_issues(): + with responses.RequestsMock() as rsps: + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1", + json=mr_content, + content_type="application/json", + status=200, + ) + rsps.add( + method=responses.GET, + url="http://localhost/api/v4/projects/1/merge_requests/1/related_issues", + json=related_issues, + content_type="application/json", + status=200, + ) + yield rsps + + def test_list_project_merge_requests(project, resp_list_merge_requests): mrs = project.mergerequests.list() assert isinstance(mrs[0], ProjectMergeRequest) @@ -115,3 +209,13 @@ def test_get_merge_request_reviewers(project, resp_get_merge_request_reviewers): assert mr.reviewers[0]["name"] == reviewers_details[0].user["name"] assert reviewers_details[0].state == "unreviewed" assert reviewers_details[0].created_at == "2022-07-27T17:03:27.684Z" + + +def test_list_related_issues(project, resp_list_merge_requests_related_issues): + mr = project.mergerequests.get(1) + this_mr_related_issues = mr.related_issues() + the_issue = next(iter(this_mr_related_issues)) + assert isinstance(mr, ProjectMergeRequest) + assert isinstance(this_mr_related_issues, RESTObjectList) + assert isinstance(the_issue, ProjectIssue) + assert the_issue.title == related_issues[0]["title"]
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: