diff --git a/src/gitlab/client.rs b/src/gitlab/client.rs index 5175c2e..095e16e 100644 --- a/src/gitlab/client.rs +++ b/src/gitlab/client.rs @@ -13,7 +13,8 @@ use tokio::time::sleep; use tracing::debug; use super::types::{ - GitLabDiscussion, GitLabIssue, GitLabMergeRequest, GitLabProject, GitLabUser, GitLabVersion, + GitLabDiscussion, GitLabIssue, GitLabLabelEvent, GitLabMergeRequest, GitLabMilestoneEvent, + GitLabProject, GitLabStateEvent, GitLabUser, GitLabVersion, }; use crate::core::error::{LoreError, Result}; @@ -550,6 +551,152 @@ impl GitLabClient { } } +/// Resource events API methods. +/// +/// These endpoints return per-entity events (not project-wide), so they collect +/// all pages into a Vec rather than using streaming. +impl GitLabClient { + /// Fetch all pages from a paginated endpoint, returning collected results. + async fn fetch_all_pages( + &self, + path: &str, + ) -> Result> { + let mut results = Vec::new(); + let mut page = 1u32; + let per_page = 100u32; + + loop { + let params = vec![ + ("per_page", per_page.to_string()), + ("page", page.to_string()), + ]; + + let (items, headers) = self + .request_with_headers::>(path, ¶ms) + .await?; + + let is_empty = items.is_empty(); + let full_page = items.len() as u32 == per_page; + results.extend(items); + + let next_page = headers + .get("x-next-page") + .and_then(|v| v.to_str().ok()) + .and_then(|s| s.parse::().ok()); + + match next_page { + Some(next) if next > page => page = next, + _ => { + if is_empty || !full_page { + break; + } + page += 1; + } + } + } + + Ok(results) + } + + /// Fetch state events for an issue. + pub async fn fetch_issue_state_events( + &self, + gitlab_project_id: i64, + iid: i64, + ) -> Result> { + let path = format!( + "/api/v4/projects/{gitlab_project_id}/issues/{iid}/resource_state_events" + ); + self.fetch_all_pages(&path).await + } + + /// Fetch label events for an issue. + pub async fn fetch_issue_label_events( + &self, + gitlab_project_id: i64, + iid: i64, + ) -> Result> { + let path = format!( + "/api/v4/projects/{gitlab_project_id}/issues/{iid}/resource_label_events" + ); + self.fetch_all_pages(&path).await + } + + /// Fetch milestone events for an issue. + pub async fn fetch_issue_milestone_events( + &self, + gitlab_project_id: i64, + iid: i64, + ) -> Result> { + let path = format!( + "/api/v4/projects/{gitlab_project_id}/issues/{iid}/resource_milestone_events" + ); + self.fetch_all_pages(&path).await + } + + /// Fetch state events for a merge request. + pub async fn fetch_mr_state_events( + &self, + gitlab_project_id: i64, + iid: i64, + ) -> Result> { + let path = format!( + "/api/v4/projects/{gitlab_project_id}/merge_requests/{iid}/resource_state_events" + ); + self.fetch_all_pages(&path).await + } + + /// Fetch label events for a merge request. + pub async fn fetch_mr_label_events( + &self, + gitlab_project_id: i64, + iid: i64, + ) -> Result> { + let path = format!( + "/api/v4/projects/{gitlab_project_id}/merge_requests/{iid}/resource_label_events" + ); + self.fetch_all_pages(&path).await + } + + /// Fetch milestone events for a merge request. + pub async fn fetch_mr_milestone_events( + &self, + gitlab_project_id: i64, + iid: i64, + ) -> Result> { + let path = format!( + "/api/v4/projects/{gitlab_project_id}/merge_requests/{iid}/resource_milestone_events" + ); + self.fetch_all_pages(&path).await + } + + /// Fetch all three event types for an entity in one call. + pub async fn fetch_all_resource_events( + &self, + gitlab_project_id: i64, + entity_type: &str, + iid: i64, + ) -> Result<(Vec, Vec, Vec)> { + match entity_type { + "issue" => { + let state = self.fetch_issue_state_events(gitlab_project_id, iid).await?; + let label = self.fetch_issue_label_events(gitlab_project_id, iid).await?; + let milestone = self.fetch_issue_milestone_events(gitlab_project_id, iid).await?; + Ok((state, label, milestone)) + } + "merge_request" => { + let state = self.fetch_mr_state_events(gitlab_project_id, iid).await?; + let label = self.fetch_mr_label_events(gitlab_project_id, iid).await?; + let milestone = self.fetch_mr_milestone_events(gitlab_project_id, iid).await?; + Ok((state, label, milestone)) + } + _ => Err(LoreError::Other(format!( + "Invalid entity type for resource events: {entity_type}" + ))), + } + } +} + /// Page result for merge request pagination. #[derive(Debug)] pub struct MergeRequestPage {