From bf5626c82f1b018af8ca0027844a2ddecbd82110 Mon Sep 17 00:00:00 2001 From: Sergi Blanch Torne Date: Mon, 15 Sep 2025 15:38:24 +0200 Subject: [PATCH] ci,marge_queue: objects to represent the queue Enhancement of the module with two structures that can encapsulate functionalities and establish links between data collected. Signed-off-by: Sergi Blanch Torne Part-of: --- bin/ci/marge_queue.py | 89 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/bin/ci/marge_queue.py b/bin/ci/marge_queue.py index 65d5c379fcc..de5bb14277f 100755 --- a/bin/ci/marge_queue.py +++ b/bin/ci/marge_queue.py @@ -12,15 +12,18 @@ Monitors Marge-bot and return number of assigned MRs. import argparse import time import sys -from datetime import datetime, timezone +from dataclasses import dataclass, field +from datetime import datetime, timedelta, timezone from dateutil import parser +from typing import Optional import gitlab -from gitlab.v4.objects import Project +from gitlab.v4.objects import Project, ProjectMergeRequest from gitlab_common import read_token, pretty_duration REFRESH_WAIT = 30 MARGE_BOT_USER_ID = 9716 +ASSIGNED_TO_MARGE = "assigned to @marge-bot" def parse_args() -> argparse.Namespace: @@ -39,6 +42,88 @@ def parse_args() -> argparse.Namespace: return parse.parse_args() +@dataclass +class MargeMergeRequest: + """Represent a Merge Request assigned to Marge""" + id: int = field(init=False) + mr: ProjectMergeRequest = field(repr=False) + updated_at: datetime | None = field(init=False, repr=False) + assigned_at: datetime | None = field(init=False, repr=False) + + def __post_init__(self): + self.id = self.mr.iid + self.updated_at = parser.parse(self.mr.updated_at) + self.assigned_at = self.__find_last_assign_to_marge() + + def __find_last_assign_to_marge(self) -> Optional[datetime]: + for note in self.mr.notes.list( + iterator=True, + order_by="updated_at", + sort="desc" + ): # start with the most recent + if note.body.startswith(ASSIGNED_TO_MARGE): + return parser.parse(note.created_at) + + def __eq__(self, other: "MargeMergeRequest") -> bool: + return self.id == other.id + + @property + def time_enqueued(self) -> timedelta: + if self.assigned_at is None: + raise ValueError("Assign to marge timestamp not defined") + return datetime.now(timezone.utc) - self.assigned_at + + @property + def web_url(self) -> str: + return self.mr.web_url + + @property + def title(self) -> str: + return self.mr.title + + +@dataclass +class MargeQueue: + """ + Collect and sort the merge requests assigned to marge + """ + elements_sorted: dict[datetime, MargeMergeRequest] = field( + init=False, repr=False, default_factory=dict + ) + undetermined: list[MargeMergeRequest] = field( + init=False, repr=False, default_factory=list + ) + + def append(self, mr: MargeMergeRequest) -> None: + if mr.assigned_at is not None: + self.elements_sorted[mr.assigned_at] = mr + else: + self.undetermined.append(mr) + + @property + def n_merge_requests_enqueued(self) -> int: + return len(self.elements_sorted) + len(self.undetermined) + + @property + def sorted_queue(self) -> list[MargeMergeRequest]: + """ + Provide a list of the elements that can be sorted based on the + assignment to Marge. + """ + return list(dict(sorted(self.elements_sorted.items())).values()) + + @property + def all_assigned( + self + ) -> list[MargeMergeRequest]: + """ + Provide a single list, but in sorted, of all the elements found + assigned to Marge. This include the elements in elements_sorted + and the ones undetermined (expected to be empty). + """ + return self.sorted_queue + self.undetermined + + def get_merge_queue(project: Project) -> int: mrs = project.mergerequests.list( assignee_id=MARGE_BOT_USER_ID,