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 tracing::debug;
|
||||||
|
|
||||||
use super::types::{
|
use super::types::{
|
||||||
GitLabDiscussion, GitLabIssue, GitLabMergeRequest, GitLabProject, GitLabUser, GitLabVersion,
|
GitLabDiscussion, GitLabIssue, GitLabLabelEvent, GitLabMergeRequest, GitLabMilestoneEvent,
|
||||||
|
GitLabProject, GitLabStateEvent, GitLabUser, GitLabVersion,
|
||||||
};
|
};
|
||||||
use crate::core::error::{LoreError, Result};
|
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.
|
/// Page result for merge request pagination.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct MergeRequestPage {
|
pub struct MergeRequestPage {
|
||||||
|
|||||||
Reference in New Issue
Block a user