diff --git a/openhands/integrations/gitlab/gitlab_service.py b/openhands/integrations/gitlab/gitlab_service.py index c075359286..b4b26aa97f 100644 --- a/openhands/integrations/gitlab/gitlab_service.py +++ b/openhands/integrations/gitlab/gitlab_service.py @@ -1,4 +1,5 @@ import os +from datetime import datetime from typing import Any import httpx @@ -7,6 +8,7 @@ from pydantic import SecretStr from openhands.integrations.service_types import ( BaseGitService, Branch, + Comment, GitService, OwnerType, ProviderType, @@ -673,6 +675,80 @@ class GitLabService(BaseGitService, GitService): # Parse the content to extract triggers from frontmatter return self._parse_microagent_content(response, file_path) + async def get_issue_comments( + self, project_id: str, issue_iid: int, limit: int = 100 + ) -> list[Comment]: + """Get the last n comments for a specific issue. + + Args: + project_id: The GitLab project ID (can be numeric ID or URL-encoded path) + issue_iid: The issue internal ID (iid) in GitLab + limit: Maximum number of comments to retrieve (default: 100) + + Returns: + List of Comment objects, ordered by creation date (newest first) + + Raises: + UnknownException: If the request fails or the issue is not found + """ + # URL-encode the project_id if it contains special characters + if '/' in str(project_id): + encoded_project_id = str(project_id).replace('/', '%2F') + else: + encoded_project_id = str(project_id) + + url = f'{self.BASE_URL}/projects/{encoded_project_id}/issues/{issue_iid}/notes' + + all_comments: list[Comment] = [] + page = 1 + per_page = min(limit, 100) # GitLab API max per_page is 100 + + while len(all_comments) < limit: + # Get comments with pagination, ordered by creation date descending + params = { + 'per_page': per_page, + 'page': page, + 'order_by': 'created_at', + 'sort': 'desc', # Get newest comments first + } + + response, headers = await self._make_request(url, params) + + if not response: # No more comments + break + + # Filter out system comments and convert to Comment objects + for comment_data in response: + if len(all_comments) >= limit: + break + + # Skip system-generated comments unless explicitly requested + if comment_data.get('system', False): + continue + + comment = Comment( + id=comment_data['id'], + body=comment_data['body'], + author=comment_data.get('author', {}).get('username', 'unknown'), + created_at=datetime.fromisoformat( + comment_data['created_at'].replace('Z', '+00:00') + ), + updated_at=datetime.fromisoformat( + comment_data['updated_at'].replace('Z', '+00:00') + ), + system=comment_data.get('system', False), + ) + all_comments.append(comment) + + # Check if we have more pages + link_header = headers.get('Link', '') + if 'rel="next"' not in link_header or len(all_comments) >= limit: + break + + page += 1 + + return all_comments + gitlab_service_cls = os.environ.get( 'OPENHANDS_GITLAB_SERVICE_CLS', diff --git a/openhands/integrations/service_types.py b/openhands/integrations/service_types.py index 55d43fdd96..20bf31c102 100644 --- a/openhands/integrations/service_types.py +++ b/openhands/integrations/service_types.py @@ -140,6 +140,15 @@ class Repository(BaseModel): main_branch: str | None = None # The main/default branch of the repository +class Comment(BaseModel): + id: int + body: str + author: str + created_at: datetime + updated_at: datetime + system: bool = False # Whether this is a system-generated comment + + class AuthenticationError(ValueError): """Raised when there is an issue with GitHub authentication."""