feat(client): Add Resource Events API endpoints with generic paginated fetcher
Extends GitLabClient with methods for fetching resource events from GitLab's per-entity API endpoints. Adds a new impl block containing: - fetch_all_pages<T>: Generic paginated collector that handles x-next-page header parsing with fallback to page-size heuristics. Uses per_page=100 and respects the existing rate limiter via request_with_headers. Terminates when: (a) x-next-page header is absent/stale, (b) response is empty, or (c) page is not full. - Six typed endpoint methods: - fetch_issue_state_events / fetch_mr_state_events - fetch_issue_label_events / fetch_mr_label_events - fetch_issue_milestone_events / fetch_mr_milestone_events - fetch_all_resource_events: Convenience method that fetches all three event types for an entity (issue or merge_request) in sequence, returning a tuple of (state, label, milestone) event vectors. Routes to issue or MR endpoints based on entity_type string. All methods follow the existing client patterns: path formatting with gitlab_project_id and iid, error propagation via Result, and rate limiter integration through the shared request_with_headers path. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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<T: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
path: &str,
|
||||
) -> Result<Vec<T>> {
|
||||
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::<Vec<T>>(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::<u32>().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<Vec<GitLabStateEvent>> {
|
||||
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<Vec<GitLabLabelEvent>> {
|
||||
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<Vec<GitLabMilestoneEvent>> {
|
||||
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<Vec<GitLabStateEvent>> {
|
||||
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<Vec<GitLabLabelEvent>> {
|
||||
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<Vec<GitLabMilestoneEvent>> {
|
||||
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<GitLabStateEvent>, Vec<GitLabLabelEvent>, Vec<GitLabMilestoneEvent>)> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user