Thanks to visit codestin.com
Credit goes to github.com

Skip to content
12 changes: 12 additions & 0 deletions docs/gl_objects/merge_trains.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Reference

+ :class:`gitlab.v4.objects.ProjectMergeTrain`
+ :class:`gitlab.v4.objects.ProjectMergeTrainManager`
+ :class:`gilab.v4.objects.ProjectMergeTrainMergeRequest`
+ :class:`gilab.v4.objects.ProjectMergeTrainMergeRequestManager`
Comment on lines +12 to +13
Copy link

Copilot AI Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the module path: 'gilab' should be spelled 'gitlab'.

Suggested change
+ :class:`gilab.v4.objects.ProjectMergeTrainMergeRequest`
+ :class:`gilab.v4.objects.ProjectMergeTrainMergeRequestManager`
+ :class:`gitlab.v4.objects.ProjectMergeTrainMergeRequest`
+ :class:`gitlab.v4.objects.ProjectMergeTrainMergeRequestManager`

Copilot uses AI. Check for mistakes.
+ :attr:`gitlab.v4.objects.Project.merge_trains`

* GitLab API: https://docs.gitlab.com/api/merge_trains
Expand All @@ -27,3 +29,13 @@ List active merge trains for a project::
List completed (have been merged) merge trains for a project::

merge_trains = project.merge_trains.list(scope="complete")

Get Merge Request Status for a Merge Train::

merge_train_mr = project.merge_trains.get(1, lazy=True).merge_requests.get(1)
merge_train_mr_status = merge_train_mr.pipeline.get("status")

Add Merge Request to a Merge Train::

merge_train_to_update = project.merge_trains.get(1, lazy=True)
merge_requests_update = merge_train_to_update.merge_requests.update(5, new_data={"sha": "cd22awr721ssds"})
39 changes: 34 additions & 5 deletions gitlab/v4/objects/merge_trains.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,43 @@
from gitlab.base import RESTObject
from gitlab.mixins import ListMixin
from gitlab.base import RESTManager, RESTObject
from gitlab.mixins import GetMixin, ListMixin, UpdateMethod, UpdateMixin
from gitlab.types import RequiredOptional

__all__ = ["ProjectMergeTrain", "ProjectMergeTrainManager"]
__all__ = [
"ProjectMergeTrain",
"ProjectMergeTrainManager",
"ProjectMergeTrainMergeRequest",
"ProjectMergeTrainMergeRequestManager",
]


class ProjectMergeTrain(RESTObject):
class ProjectMergeTrainMergeRequest(RESTObject):
pass


class ProjectMergeTrainManager(ListMixin[ProjectMergeTrain]):
class ProjectMergeTrainMergeRequestManager(
GetMixin[ProjectMergeTrainMergeRequest],
UpdateMixin[ProjectMergeTrainMergeRequest],
RESTManager[ProjectMergeTrainMergeRequest],
):
_path = "/projects/{project_id}/merge_trains/merge_requests"
_obj_cls = ProjectMergeTrainMergeRequest
_from_parent_attrs = {"project_id": "project_id"}
_update_method: UpdateMethod = UpdateMethod.POST

_update_attrs = RequiredOptional(
optional=("sha", "squash", "when_pipeline_succeeds")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

)


class ProjectMergeTrain(RESTObject):
merge_requests: ProjectMergeTrainMergeRequestManager


class ProjectMergeTrainManager(
GetMixin[ProjectMergeTrain],
ListMixin[ProjectMergeTrain],
RESTManager[ProjectMergeTrain],
):
_path = "/projects/{project_id}/merge_trains"
_obj_cls = ProjectMergeTrain
_from_parent_attrs = {"project_id": "id"}
Expand Down
2 changes: 1 addition & 1 deletion gitlab/v4/objects/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
ProjectApprovalRuleManager,
)
from .merge_requests import ProjectMergeRequestManager # noqa: F401
from .merge_trains import ProjectMergeTrainManager # noqa: F401
from .merge_trains import ProjectMergeTrainManager
from .milestones import ProjectMilestoneManager # noqa: F401
from .notes import ProjectNoteManager # noqa: F401
from .notification_settings import ProjectNotificationSettingsManager # noqa: F401
Expand Down
62 changes: 60 additions & 2 deletions tests/unit/objects/test_merge_trains.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
import pytest
import responses

from gitlab.v4.objects import ProjectMergeTrain
from gitlab.v4.objects import ProjectMergeTrain, ProjectMergeTrainMergeRequest

mr_content = {
"id": 110,
"merge_request": {
"id": 1,
"id": 273,
"iid": 1,
"project_id": 3,
"title": "Test merge train",
Expand Down Expand Up @@ -46,6 +46,10 @@
"duration": 70,
}

merge_train_update = mr_content.copy()
Copy link

Copilot AI Jun 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using dict.copy() here performs a shallow copy, so nested structures (like the 'pipeline' dict) remain shared with mr_content and may be mutated unexpectedly. Consider using copy.deepcopy() to isolate test data changes.

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably a good idea. Since line 51 would modify the underlying dict.

merge_train_update["iid"] = 4
merge_train_update["pipeline"]["sha"] = "ef33a3zxc3"


@pytest.fixture
def resp_list_merge_trains():
Expand All @@ -60,7 +64,61 @@ def resp_list_merge_trains():
yield rsps


@pytest.fixture
def resp_merge_trains_merge_request_get():
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.GET,
url="http://localhost/api/v4/projects/1/merge_trains/merge_requests/1",
json=mr_content,
content_type="application/json",
status=200,
)
yield rsps


@pytest.fixture
def resp_merge_trains_merge_request_post():
with responses.RequestsMock() as rsps:
rsps.add(
method=responses.POST,
url="http://localhost/api/v4/projects/1/merge_trains/merge_requests/4",
json=[merge_train_update],
content_type="application/json",
status=200,
)
yield rsps


def test_list_project_merge_requests(project, resp_list_merge_trains):
merge_trains = project.merge_trains.list()
assert isinstance(merge_trains[0], ProjectMergeTrain)
assert merge_trains[0].id == mr_content["id"]


def test_merge_trains_status_merge_request(
project, resp_merge_trains_merge_request_get
):
merge_train_mr: ProjectMergeTrainMergeRequest = project.merge_trains.get(
1, lazy=True
).merge_requests.get(1)
assert isinstance(merge_train_mr, ProjectMergeTrainMergeRequest)
assert merge_train_mr.get_id() == 110
assert merge_train_mr.merge_request["iid"] == mr_content["merge_request"]["iid"]
assert merge_train_mr.pipeline.get("status") == mr_content["pipeline"]["status"]


def test_merge_train_add_merge_request(project, resp_merge_trains_merge_request_post):
merge_train: ProjectMergeTrain = project.merge_trains.get(1, lazy=True)
merge_requests_update = merge_train.merge_requests.update(
4, new_data={"sha": "ef33a3zxc3"}
)
assert isinstance(merge_train, ProjectMergeTrain)
assert (
merge_requests_update[0]["pipeline"]["sha"]
== merge_train_update["pipeline"]["sha"]
)
assert (
merge_requests_update[0]["merge_request"]["iid"]
== merge_train_update["merge_request"]["iid"]
)