From ea589f2ca5363894a59ed79866e7d1c2d3236181 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 22:07:35 +1200 Subject: [PATCH 01/11] feat: introduce related_issues to merge requests --- docs/gl_objects/merge_requests.rst | 4 ++++ gitlab/v4/objects/merge_requests.py | 29 +++++++++++++++++++++++ tests/unit/objects/test_merge_requests.py | 8 +++++++ 3 files changed, 41 insertions(+) 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..62538931a 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -40,6 +40,9 @@ "web_url": "http://gitlab.example.com//kenyatta_oconnell", } ], + "related_issues": [ + 1, 2 + ] } reviewers_content = [ @@ -93,6 +96,11 @@ def resp_get_merge_request_reviewers(): yield rsps +def test_list_related_issues(project, resp_list_merge_requests): + mrs = project.mergerequests.list() + assert isinstance(mrs[0], ProjectMergeRequest) + assert mrs[0].related_issues == mr_content["related_issues"] + def test_list_project_merge_requests(project, resp_list_merge_requests): mrs = project.mergerequests.list() assert isinstance(mrs[0], ProjectMergeRequest) From 73c13d22e6e04b10364d1c26ad3e63b47da08b8f Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 22:24:25 +1200 Subject: [PATCH 02/11] chore: fix black lint errors Ensure black lint check passes following PEP-8 --- tests/unit/objects/test_merge_requests.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index 62538931a..b3eed90fd 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -40,9 +40,7 @@ "web_url": "http://gitlab.example.com//kenyatta_oconnell", } ], - "related_issues": [ - 1, 2 - ] + "related_issues": [1, 2], } reviewers_content = [ @@ -101,6 +99,7 @@ def test_list_related_issues(project, resp_list_merge_requests): assert isinstance(mrs[0], ProjectMergeRequest) assert mrs[0].related_issues == mr_content["related_issues"] + def test_list_project_merge_requests(project, resp_list_merge_requests): mrs = project.mergerequests.list() assert isinstance(mrs[0], ProjectMergeRequest) From 0b74c5956bcae89badce33016b1193fb0608f070 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 22:51:39 +1200 Subject: [PATCH 03/11] chore: revert related_issues test --- tests/unit/objects/test_merge_requests.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index b3eed90fd..6f8a6a7de 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -40,7 +40,6 @@ "web_url": "http://gitlab.example.com//kenyatta_oconnell", } ], - "related_issues": [1, 2], } reviewers_content = [ @@ -94,12 +93,6 @@ def resp_get_merge_request_reviewers(): yield rsps -def test_list_related_issues(project, resp_list_merge_requests): - mrs = project.mergerequests.list() - assert isinstance(mrs[0], ProjectMergeRequest) - assert mrs[0].related_issues == mr_content["related_issues"] - - def test_list_project_merge_requests(project, resp_list_merge_requests): mrs = project.mergerequests.list() assert isinstance(mrs[0], ProjectMergeRequest) From 9aba4a127eb51eed1f417c26faa99be90abbec8d Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 23:14:50 +1200 Subject: [PATCH 04/11] test: update merge requests with related_issues Add related_issues mock endpoint call and resp test --- tests/unit/objects/test_merge_requests.py | 104 ++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index 6f8a6a7de..57b5b0597 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -13,6 +13,7 @@ ProjectDeploymentMergeRequest, ProjectMergeRequest, ProjectMergeRequestReviewerDetail, + ProjectIssue, ) mr_content = { @@ -57,6 +58,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 +166,28 @@ 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=re.compile( + r"http://localhost/api/v4/projects/1/(deployments/1/)?merge_requests" + ), + 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 +210,12 @@ 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_issue = mr.related_issues.list() + assert isinstance(mr, ProjectMergeRequest) + assert isinstance(this_mr_related_issue, list) + assert isinstance(this_mr_related_issue[0], ProjectIssue) + assert this_mr_related_issue[0]["title"] == related_issues[0]["title"] From 7f4a0556c8040ce14a45b88399fd1be44e378e06 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 23:24:21 +1200 Subject: [PATCH 05/11] chore: update related_issues test attrs & request Update title attr and ensure GET request is mocked --- tests/unit/objects/test_merge_requests.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index 57b5b0597..dd7e389c2 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -171,9 +171,7 @@ def resp_list_merge_requests_related_issues(): with responses.RequestsMock() as rsps: rsps.add( method=responses.GET, - url=re.compile( - r"http://localhost/api/v4/projects/1/(deployments/1/)?merge_requests" - ), + url="http://localhost/api/v4/projects/1/merge_requests/1", json=[mr_content], content_type="application/json", status=200, @@ -214,8 +212,8 @@ def test_get_merge_request_reviewers(project, resp_get_merge_request_reviewers): def test_list_related_issues(project, resp_list_merge_requests_related_issues): mr = project.mergerequests.get(1) - this_mr_related_issue = mr.related_issues.list() + this_mrs_related_issues = mr.related_issues.list() assert isinstance(mr, ProjectMergeRequest) - assert isinstance(this_mr_related_issue, list) - assert isinstance(this_mr_related_issue[0], ProjectIssue) - assert this_mr_related_issue[0]["title"] == related_issues[0]["title"] + assert isinstance(this_mrs_related_issues, list) + assert isinstance(this_mrs_related_issues[0], ProjectIssue) + assert this_mr_related_issue[0].title == related_issues[0]["title"] From a2d54ea208fa1ea0d6c79d507d1620d174708222 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 23:38:08 +1200 Subject: [PATCH 06/11] test: update merge_requests ProjectIssue obj --- tests/unit/objects/test_merge_requests.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index dd7e389c2..addd20103 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -75,7 +75,7 @@ "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", + "web_url": "http://gitlab.example.com/kenyatta_oconnell", }, "labels": [ "FakeCategory", @@ -88,7 +88,7 @@ "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", + "web_url": "http://gitlab.example.com/kenyatta_oconnell", } ], "author": { @@ -211,9 +211,9 @@ def test_get_merge_request_reviewers(project, resp_get_merge_request_reviewers): def test_list_related_issues(project, resp_list_merge_requests_related_issues): - mr = project.mergerequests.get(1) - this_mrs_related_issues = mr.related_issues.list() - assert isinstance(mr, ProjectMergeRequest) - assert isinstance(this_mrs_related_issues, list) - assert isinstance(this_mrs_related_issues[0], ProjectIssue) - assert this_mr_related_issue[0].title == related_issues[0]["title"] + mrs = project.mergerequests.list() + this_mr_related_issues = mrs[0].related_issues.list() + assert isinstance(mrs[0], ProjectMergeRequest) + assert isinstance(this_mr_related_issues, list) + assert isinstance(this_mr_related_issues[0], ProjectIssue) + assert this_mr_related_issues[0].title == related_issues[0]["title"] From 6f7804c5b738128843b5e42cfe74cd3111455497 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 23:46:41 +1200 Subject: [PATCH 07/11] test: update URL request for related_issues Ensure related_issues URL calls match expected mocks --- tests/unit/objects/test_merge_requests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index addd20103..56e968629 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -172,13 +172,13 @@ def resp_list_merge_requests_related_issues(): rsps.add( method=responses.GET, url="http://localhost/api/v4/projects/1/merge_requests/1", - json=[mr_content], + 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", + url="http://localhost/api/v4/projects/3/merge_requests/1/related_issues", json=related_issues, content_type="application/json", status=200, @@ -211,9 +211,9 @@ def test_get_merge_request_reviewers(project, resp_get_merge_request_reviewers): def test_list_related_issues(project, resp_list_merge_requests_related_issues): - mrs = project.mergerequests.list() - this_mr_related_issues = mrs[0].related_issues.list() - assert isinstance(mrs[0], ProjectMergeRequest) + mr = project.mergerequests.get(1) + this_mr_related_issues = mr.related_issues.list() + assert isinstance(mr, ProjectMergeRequest) assert isinstance(this_mr_related_issues, list) assert isinstance(this_mr_related_issues[0], ProjectIssue) assert this_mr_related_issues[0].title == related_issues[0]["title"] From febc0bf3291d02e5f5fb05d15e98b68661bed920 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 23:52:40 +1200 Subject: [PATCH 08/11] chore: ensure related_issues returns expected list --- tests/unit/objects/test_merge_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index 56e968629..b0db2e13a 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -212,7 +212,7 @@ def test_get_merge_request_reviewers(project, resp_get_merge_request_reviewers): def test_list_related_issues(project, resp_list_merge_requests_related_issues): mr = project.mergerequests.get(1) - this_mr_related_issues = mr.related_issues.list() + this_mr_related_issues = mr.related_issues() assert isinstance(mr, ProjectMergeRequest) assert isinstance(this_mr_related_issues, list) assert isinstance(this_mr_related_issues[0], ProjectIssue) From cd5a5d10401fbe3808fa06e80487c549c08e648c Mon Sep 17 00:00:00 2001 From: xakepnz Date: Mon, 23 Sep 2024 23:57:42 +1200 Subject: [PATCH 09/11] chore: test mock url matches Ensure related_issues mock request matches the same project & MR mock request --- tests/unit/objects/test_merge_requests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index b0db2e13a..d0287b89f 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -178,7 +178,7 @@ def resp_list_merge_requests_related_issues(): ) rsps.add( method=responses.GET, - url="http://localhost/api/v4/projects/3/merge_requests/1/related_issues", + url="http://localhost/api/v4/projects/1/merge_requests/1/related_issues", json=related_issues, content_type="application/json", status=200, From fd42631800607d62bd98fec4922e3a8a2eda5ca6 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Tue, 24 Sep 2024 00:14:37 +1200 Subject: [PATCH 10/11] chore: add RESTObjectList instance check --- tests/unit/objects/test_merge_requests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index d0287b89f..93f9e848f 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -15,6 +15,7 @@ ProjectMergeRequestReviewerDetail, ProjectIssue, ) +from gitlab.base import RESTObjectList mr_content = { "id": 1, @@ -214,6 +215,6 @@ def test_list_related_issues(project, resp_list_merge_requests_related_issues): mr = project.mergerequests.get(1) this_mr_related_issues = mr.related_issues() assert isinstance(mr, ProjectMergeRequest) - assert isinstance(this_mr_related_issues, list) + assert isinstance(this_mr_related_issues, RESTObjectList) assert isinstance(this_mr_related_issues[0], ProjectIssue) assert this_mr_related_issues[0].title == related_issues[0]["title"] From 412d92d08c88d099f8bb361eb9e62db365674448 Mon Sep 17 00:00:00 2001 From: xakepnz Date: Tue, 24 Sep 2024 00:32:06 +1200 Subject: [PATCH 11/11] chore: ensure related_issues returns expected results RESTObjectList iters an expected ProjectIssue obj --- tests/unit/objects/test_merge_requests.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/unit/objects/test_merge_requests.py b/tests/unit/objects/test_merge_requests.py index 93f9e848f..400b24b34 100644 --- a/tests/unit/objects/test_merge_requests.py +++ b/tests/unit/objects/test_merge_requests.py @@ -9,13 +9,13 @@ import pytest import responses +from gitlab.base import RESTObjectList from gitlab.v4.objects import ( ProjectDeploymentMergeRequest, + ProjectIssue, ProjectMergeRequest, ProjectMergeRequestReviewerDetail, - ProjectIssue, ) -from gitlab.base import RESTObjectList mr_content = { "id": 1, @@ -214,7 +214,8 @@ def test_get_merge_request_reviewers(project, resp_get_merge_request_reviewers): 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(this_mr_related_issues[0], ProjectIssue) - assert this_mr_related_issues[0].title == related_issues[0]["title"] + assert isinstance(the_issue, ProjectIssue) + assert the_issue.title == related_issues[0]["title"]