2026-05-06 01:42:38 +08:00
|
|
|
use std::ops::Deref;
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
use graphql_client::GraphQLQuery;
|
2026-05-06 01:42:38 +08:00
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
2026-05-08 02:23:28 +08:00
|
|
|
api::{
|
|
|
|
|
self,
|
2026-05-11 00:32:12 +08:00
|
|
|
issues::{
|
|
|
|
|
pull_request_pagination_query::PullRequestPaginationQuerySearchEdgesNode,
|
|
|
|
|
pull_request_query::PullRequestQueryNode,
|
|
|
|
|
},
|
2026-05-08 02:23:28 +08:00
|
|
|
},
|
2026-05-06 01:42:38 +08:00
|
|
|
query,
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
type DateTime = String;
|
|
|
|
|
type URI = String;
|
2026-05-18 22:30:46 +08:00
|
|
|
type GitObjectID = String;
|
2026-05-11 00:32:12 +08:00
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize)]
|
2026-05-06 01:42:38 +08:00
|
|
|
#[serde(transparent)]
|
|
|
|
|
#[repr(transparent)]
|
2026-05-08 02:23:28 +08:00
|
|
|
pub(crate) struct Id(String);
|
2026-05-06 01:42:38 +08:00
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
impl Deref for Id {
|
|
|
|
|
type Target = String;
|
2026-05-06 01:42:38 +08:00
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
|
&self.0
|
|
|
|
|
}
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
impl From<&str> for Id {
|
|
|
|
|
fn from(value: &str) -> Self {
|
|
|
|
|
Self(value.to_owned())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<String> for Id {
|
|
|
|
|
fn from(value: String) -> Self {
|
|
|
|
|
Self(value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<Id> for String {
|
|
|
|
|
fn from(value: Id) -> Self {
|
|
|
|
|
value.0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
#[derive(Debug, Deserialize)]
|
2026-05-08 02:23:28 +08:00
|
|
|
pub(crate) struct PullRequestPaginatedResponse {
|
|
|
|
|
pub(crate) items: Vec<PullRequest>,
|
|
|
|
|
pub(crate) start_cursor: Option<String>,
|
|
|
|
|
pub(crate) end_cursor: Option<String>,
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub(crate) struct PullRequest {
|
2026-05-11 00:32:12 +08:00
|
|
|
pub(crate) id: Id,
|
2026-05-08 02:23:28 +08:00
|
|
|
pub(crate) title: String,
|
2026-05-11 00:32:12 +08:00
|
|
|
pub(crate) state: PullRequestState,
|
2026-05-08 02:23:28 +08:00
|
|
|
pub(crate) is_draft: bool,
|
|
|
|
|
pub(crate) repo_slug: String,
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
|
pub(crate) struct DetailedPullRequest {
|
|
|
|
|
pub(crate) title: String,
|
|
|
|
|
pub(crate) state: PullRequestState,
|
|
|
|
|
pub(crate) is_draft: bool,
|
|
|
|
|
pub(crate) body: String,
|
2026-05-12 02:19:08 +08:00
|
|
|
pub(crate) created_at: Option<chrono::DateTime<chrono::FixedOffset>>,
|
2026-05-12 01:34:33 +08:00
|
|
|
pub(crate) author: Option<super::user::Actor>,
|
2026-05-18 22:30:46 +08:00
|
|
|
pub(crate) base_branch_name: String,
|
|
|
|
|
pub(crate) base_repo_slug: String,
|
|
|
|
|
pub(crate) base_ref: String,
|
|
|
|
|
pub(crate) head_branch_name: String,
|
|
|
|
|
pub(crate) head_ref: String,
|
|
|
|
|
pub(crate) head_repo_slug: String,
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub(crate) struct PullRequestTimeline {
|
|
|
|
|
pub(crate) items: Vec<PullRequestTimelineItem>,
|
|
|
|
|
pub(crate) end_cursor: Option<String>,
|
|
|
|
|
pub(crate) has_next_page: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub(crate) enum PullRequestTimelineItem {
|
|
|
|
|
Assigned {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
assignee: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Unassigned {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
assignee: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Comment {
|
|
|
|
|
created_at: String,
|
|
|
|
|
author: Option<TimelineActor>,
|
|
|
|
|
body: String,
|
|
|
|
|
},
|
|
|
|
|
Commit {
|
|
|
|
|
committed_at: String,
|
|
|
|
|
abbreviated_oid: String,
|
|
|
|
|
message_headline: String,
|
|
|
|
|
},
|
|
|
|
|
Review {
|
|
|
|
|
created_at: String,
|
|
|
|
|
author: Option<TimelineActor>,
|
|
|
|
|
state: String,
|
|
|
|
|
body: String,
|
|
|
|
|
},
|
|
|
|
|
ReviewRequested {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
reviewer: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
ReviewRequestRemoved {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
reviewer: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
ReviewDismissed {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Merged {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Closed {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Reopened {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
ConvertToDraft {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
ReadyForReview {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
HeadRefForcePushed {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
before_commit_oid: Option<String>,
|
|
|
|
|
after_commit_oid: Option<String>,
|
|
|
|
|
},
|
|
|
|
|
BaseRefChanged {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Labeled {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
label: String,
|
|
|
|
|
},
|
|
|
|
|
Unlabeled {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
label: String,
|
|
|
|
|
},
|
|
|
|
|
Milestoned {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
milestone_title: String,
|
|
|
|
|
},
|
|
|
|
|
Demilestoned {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
milestone_title: String,
|
|
|
|
|
},
|
|
|
|
|
Referenced {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
CrossReferenced {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
AutoMergeEnabled {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
AutoMergeDisabled {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
reason: String,
|
|
|
|
|
},
|
|
|
|
|
AddedToMergeQueue {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
RemovedFromMergeQueue {
|
|
|
|
|
created_at: String,
|
|
|
|
|
actor: Option<TimelineActor>,
|
|
|
|
|
},
|
|
|
|
|
Other {
|
|
|
|
|
typename: String,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
pub(crate) struct ChangedFile {
|
|
|
|
|
pub(crate) cursor: String,
|
|
|
|
|
pub(crate) change_type: ChangeType,
|
|
|
|
|
pub(crate) additions: i64,
|
|
|
|
|
pub(crate) deletions: i64,
|
|
|
|
|
pub(crate) path: String,
|
|
|
|
|
pub(crate) viewer_viewed_state: FileViewedState,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
|
|
|
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
|
|
|
pub(crate) enum FileViewedState {
|
|
|
|
|
Dismissed,
|
|
|
|
|
Viewed,
|
|
|
|
|
Unviewed,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
|
|
|
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
|
|
|
pub(crate) enum ChangeType {
|
|
|
|
|
Added,
|
|
|
|
|
Modified,
|
|
|
|
|
Deleted,
|
|
|
|
|
Renamed,
|
|
|
|
|
Copied,
|
|
|
|
|
Changed,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
|
|
|
pub(crate) struct TimelineActor {
|
|
|
|
|
pub(crate) kind: String,
|
|
|
|
|
pub(crate) name: String,
|
|
|
|
|
pub(crate) avatar_url: Option<String>,
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::fmt::Display for Id {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
self.0.fmt(f)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize)]
|
|
|
|
|
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
|
|
|
|
pub(crate) enum PullRequestState {
|
|
|
|
|
Open,
|
|
|
|
|
Closed,
|
|
|
|
|
Merged,
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
#[derive(graphql_client::GraphQLQuery)]
|
|
|
|
|
#[graphql(
|
|
|
|
|
schema_path = "src/api/graphql/schema.json",
|
|
|
|
|
query_path = "src/api/graphql/list_pull_requests.graphql"
|
2026-05-11 00:32:12 +08:00
|
|
|
extern_enums("PullRequestState")
|
|
|
|
|
)]
|
|
|
|
|
struct PullRequestPaginationQuery;
|
|
|
|
|
|
|
|
|
|
#[derive(graphql_client::GraphQLQuery)]
|
|
|
|
|
#[graphql(
|
|
|
|
|
schema_path = "src/api/graphql/schema.json",
|
|
|
|
|
query_path = "src/api/graphql/fetch_pull_request.graphql"
|
|
|
|
|
extern_enums("PullRequestState")
|
2026-05-08 02:23:28 +08:00
|
|
|
)]
|
|
|
|
|
struct PullRequestQuery;
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
#[derive(graphql_client::GraphQLQuery)]
|
|
|
|
|
#[graphql(
|
|
|
|
|
schema_path = "src/api/graphql/schema.json",
|
|
|
|
|
query_path = "src/api/graphql/fetch_pull_request_timeline.graphql"
|
|
|
|
|
)]
|
|
|
|
|
struct PullRequestTimelineQuery;
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
#[derive(graphql_client::GraphQLQuery)]
|
|
|
|
|
#[graphql(
|
|
|
|
|
schema_path = "src/api/graphql/schema.json",
|
|
|
|
|
query_path = "src/api/graphql/fetch_pull_request_file_tree.graphql",
|
|
|
|
|
extern_enums("FileViewedState")
|
|
|
|
|
)]
|
|
|
|
|
struct PullRequestFileTreeQuery;
|
|
|
|
|
|
|
|
|
|
pub(super) type PullRequestFileTreeResponse = pull_request_file_tree_query::ResponseData;
|
2026-05-11 00:32:12 +08:00
|
|
|
pub(super) type PullRequestTimelineResponse = pull_request_timeline_query::ResponseData;
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
pub(super) type PullRequestTimelineResponseNode = PullRequestTimelineQueryNode;
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
pub(super) type PullRequestTimelineNode =
|
|
|
|
|
pull_request_timeline_query::PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes;
|
|
|
|
|
|
|
|
|
|
use self::pull_request_timeline_query::{
|
|
|
|
|
PullRequestReviewState, PullRequestTimelineQueryNode,
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes, actorFields, actorFieldsOn,
|
|
|
|
|
assigneeFields, requestedReviewerFields,
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct ListPullRequests {
|
|
|
|
|
pub filter: Option<&'static str>,
|
|
|
|
|
pub page: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl query::QueryFn for ListPullRequests {
|
2026-05-08 02:23:28 +08:00
|
|
|
type Data = PullRequestPaginatedResponse;
|
2026-05-06 01:42:38 +08:00
|
|
|
type Error = api::Error;
|
|
|
|
|
type Context = api::QueryContext;
|
|
|
|
|
|
|
|
|
|
fn key(&self) -> query::Key {
|
|
|
|
|
format!(
|
|
|
|
|
"issues/list?pulls=true&page={}&filter={}",
|
|
|
|
|
self.page,
|
|
|
|
|
self.filter.unwrap_or_default()
|
|
|
|
|
)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
if c.should_use_fixtures {
|
|
|
|
|
return super::mock::list_pull_requests(self.filter, self.page);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 02:23:28 +08:00
|
|
|
let query_string = match self.filter {
|
2026-05-18 22:30:46 +08:00
|
|
|
| Some(filter) => format!("is:pr archived:false sort:updated-desc {}", filter),
|
|
|
|
|
| None => "is:pr archived:false sort:updated-desc".into(),
|
2026-05-08 02:23:28 +08:00
|
|
|
};
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
let gql =
|
|
|
|
|
PullRequestPaginationQuery::build_query(pull_request_pagination_query::Variables {
|
|
|
|
|
query: query_string,
|
|
|
|
|
});
|
2026-05-06 01:42:38 +08:00
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
let res = c.github_graphql_request(&gql)?.send().await?;
|
2026-05-06 01:42:38 +08:00
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
let (_, data) =
|
|
|
|
|
api::parse_graphql_response::<pull_request_pagination_query::ResponseData>(res).await?;
|
2026-05-08 02:23:28 +08:00
|
|
|
|
|
|
|
|
Ok(PullRequestPaginatedResponse {
|
|
|
|
|
items: data
|
|
|
|
|
.search
|
|
|
|
|
.edges
|
|
|
|
|
.map(|it| {
|
|
|
|
|
it.into_iter()
|
|
|
|
|
.flatten()
|
|
|
|
|
.filter_map(|edge| {
|
|
|
|
|
edge.node.and_then(|n| match n {
|
2026-05-18 22:30:46 +08:00
|
|
|
| PullRequestPaginationQuerySearchEdgesNode::PullRequest(p) => {
|
|
|
|
|
Some(PullRequest {
|
|
|
|
|
id: p.id.into(),
|
|
|
|
|
title: p.title,
|
|
|
|
|
state: p.state,
|
|
|
|
|
is_draft: p.is_draft,
|
|
|
|
|
repo_slug: format!(
|
|
|
|
|
"{}/{}",
|
|
|
|
|
p.repository.owner.login, p.repository.name
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
| _ => None,
|
2026-05-08 02:23:28 +08:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
start_cursor: data.search.page_info.start_cursor,
|
|
|
|
|
end_cursor: data.search.page_info.end_cursor,
|
|
|
|
|
})
|
2026-05-06 01:42:38 +08:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-11 00:32:12 +08:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct FetchPullRequest {
|
|
|
|
|
pub(crate) id: Id,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl query::QueryFn for FetchPullRequest {
|
|
|
|
|
type Data = DetailedPullRequest;
|
|
|
|
|
type Error = api::Error;
|
|
|
|
|
type Context = api::QueryContext;
|
|
|
|
|
|
|
|
|
|
fn key(&self) -> query::Key {
|
|
|
|
|
format!("issues/{}", self.id).into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
if c.should_use_fixtures {
|
|
|
|
|
return super::mock::fetch_pull_request(&self.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let gql = PullRequestQuery::build_query(pull_request_query::Variables {
|
|
|
|
|
id: self.id.clone().into(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let res = c.github_graphql_request(&gql)?.send().await?;
|
|
|
|
|
let (_, data) =
|
|
|
|
|
api::parse_graphql_response::<pull_request_query::ResponseData>(res).await?;
|
|
|
|
|
|
|
|
|
|
data.node
|
|
|
|
|
.ok_or(api::Error::MalformedResponse(
|
|
|
|
|
"missing 'node' field on PullRequestQuery response".into(),
|
|
|
|
|
))
|
|
|
|
|
.and_then(|n| match n {
|
2026-05-18 22:30:46 +08:00
|
|
|
| PullRequestQueryNode::PullRequest(p) => {
|
|
|
|
|
let created_at =
|
|
|
|
|
chrono::DateTime::parse_from_rfc3339(&p.created_at).map_err(|err| {
|
|
|
|
|
api::Error::MalformedResponse(format!(
|
|
|
|
|
"invalid pull request createdAt {:?}: {err}",
|
|
|
|
|
p.created_at
|
|
|
|
|
))
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(DetailedPullRequest {
|
|
|
|
|
title: p.title,
|
|
|
|
|
state: p.state,
|
|
|
|
|
is_draft: p.is_draft,
|
|
|
|
|
body: p.body,
|
|
|
|
|
author: p.author.map(|it| api::user::Actor {
|
|
|
|
|
login: it.login,
|
|
|
|
|
avatar_url: it.avatar_url,
|
|
|
|
|
}),
|
|
|
|
|
base_repo_slug: p
|
|
|
|
|
.base_repository
|
|
|
|
|
.map(|it| it.name_with_owner)
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
base_branch_name: p.base_ref_name,
|
|
|
|
|
base_ref: p.base_ref_oid,
|
|
|
|
|
head_repo_slug: p
|
|
|
|
|
.head_repository
|
|
|
|
|
.map(|it| it.name_with_owner)
|
|
|
|
|
.unwrap_or_default(),
|
|
|
|
|
head_branch_name: p.head_ref_name,
|
|
|
|
|
head_ref: p.head_ref_oid,
|
|
|
|
|
created_at: Some(created_at),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
| _ => Err(api::Error::MalformedResponse(
|
|
|
|
|
"unexpected node type on PullRequestQuery".into(),
|
|
|
|
|
)),
|
2026-05-11 00:32:12 +08:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
2026-05-18 22:30:46 +08:00
|
|
|
pub(crate) struct FetchPullRequestFileTree {
|
2026-05-11 00:32:12 +08:00
|
|
|
pub(crate) id: Id,
|
|
|
|
|
pub(crate) first: i64,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
impl query::QueryFn for FetchPullRequestFileTree {
|
|
|
|
|
type Data = Vec<ChangedFile>;
|
|
|
|
|
type Error = api::Error;
|
|
|
|
|
type Context = api::QueryContext;
|
|
|
|
|
|
|
|
|
|
fn key(&self) -> query::Key {
|
|
|
|
|
format!("issues/{}/files?first={}", self.id, self.first).into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
let data = if c.should_use_fixtures {
|
|
|
|
|
super::mock::fetch_pull_request_file_tree(&self.id)?
|
|
|
|
|
} else {
|
|
|
|
|
let gql =
|
|
|
|
|
PullRequestFileTreeQuery::build_query(pull_request_file_tree_query::Variables {
|
|
|
|
|
id: self.id.clone().into(),
|
|
|
|
|
first: self.first,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let res = c.github_graphql_request(&gql)?.send().await?;
|
|
|
|
|
|
|
|
|
|
api::parse_graphql_response::<PullRequestFileTreeResponse>(res)
|
|
|
|
|
.await?
|
|
|
|
|
.1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
|
|
|
let data = {
|
|
|
|
|
let gql =
|
|
|
|
|
PullRequestFileTreeQuery::build_query(pull_request_file_tree_query::Variables {
|
|
|
|
|
id: self.id.clone().into(),
|
|
|
|
|
first: self.first,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let res = c.github_graphql_request(&gql)?.send().await?;
|
|
|
|
|
|
|
|
|
|
api::parse_graphql_response::<PullRequestFileTreeResponse>(res)
|
|
|
|
|
.await?
|
|
|
|
|
.1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let pull_request = data
|
|
|
|
|
.node
|
|
|
|
|
.ok_or(api::Error::MalformedResponse(
|
|
|
|
|
"missing 'node' field on PullRequestFileTreeQuery response".into(),
|
|
|
|
|
))
|
|
|
|
|
.and_then(|node| match node {
|
|
|
|
|
| pull_request_file_tree_query::PullRequestFileTreeQueryNode::PullRequest(
|
|
|
|
|
pull_request,
|
|
|
|
|
) => Ok(pull_request),
|
|
|
|
|
| _ => Err(api::Error::MalformedResponse(
|
|
|
|
|
"unexpected node type on PullRequestFileTreeQuery".into(),
|
|
|
|
|
)),
|
|
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
Ok(pull_request
|
|
|
|
|
.files
|
|
|
|
|
.and_then(|files| files.edges)
|
|
|
|
|
.map(|it| {
|
|
|
|
|
it.into_iter()
|
|
|
|
|
.flatten()
|
|
|
|
|
.filter_map(|edge| {
|
|
|
|
|
let cursor = edge.cursor;
|
|
|
|
|
edge.node.map(|node| ChangedFile {
|
|
|
|
|
cursor,
|
|
|
|
|
change_type: match node.change_type {
|
|
|
|
|
| pull_request_file_tree_query::PatchStatus::ADDED => ChangeType::Added,
|
|
|
|
|
| pull_request_file_tree_query::PatchStatus::MODIFIED => {
|
|
|
|
|
ChangeType::Modified
|
|
|
|
|
}
|
|
|
|
|
| pull_request_file_tree_query::PatchStatus::DELETED => {
|
|
|
|
|
ChangeType::Deleted
|
|
|
|
|
}
|
|
|
|
|
| pull_request_file_tree_query::PatchStatus::RENAMED => {
|
|
|
|
|
ChangeType::Renamed
|
|
|
|
|
}
|
|
|
|
|
| pull_request_file_tree_query::PatchStatus::COPIED => {
|
|
|
|
|
ChangeType::Copied
|
|
|
|
|
}
|
|
|
|
|
| pull_request_file_tree_query::PatchStatus::CHANGED => {
|
|
|
|
|
ChangeType::Changed
|
|
|
|
|
}
|
|
|
|
|
| _ => ChangeType::Changed,
|
|
|
|
|
},
|
|
|
|
|
additions: node.additions,
|
|
|
|
|
deletions: node.deletions,
|
|
|
|
|
path: node.path,
|
|
|
|
|
viewer_viewed_state: node.viewer_viewed_state,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default())
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub(crate) struct FetchPullRequestTimeline {
|
|
|
|
|
pub(crate) id: Id,
|
|
|
|
|
pub(crate) first: i64,
|
|
|
|
|
pub(crate) after: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 00:32:12 +08:00
|
|
|
impl query::QueryFn for FetchPullRequestTimeline {
|
|
|
|
|
type Data = PullRequestTimeline;
|
|
|
|
|
type Error = api::Error;
|
|
|
|
|
type Context = api::QueryContext;
|
|
|
|
|
|
|
|
|
|
fn key(&self) -> query::Key {
|
|
|
|
|
format!(
|
|
|
|
|
"issues/{}/timeline?first={}&after={}",
|
|
|
|
|
self.id,
|
|
|
|
|
self.first,
|
|
|
|
|
self.after.as_deref().unwrap_or_default()
|
|
|
|
|
)
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
|
|
|
|
|
fn normalize_actor(actor: actorFields) -> TimelineActor {
|
|
|
|
|
let actorFields {
|
|
|
|
|
login,
|
|
|
|
|
avatar_url,
|
|
|
|
|
on,
|
|
|
|
|
} = actor;
|
|
|
|
|
|
|
|
|
|
TimelineActor {
|
|
|
|
|
kind: match on {
|
2026-05-18 22:30:46 +08:00
|
|
|
| actorFieldsOn::Bot => "Bot",
|
|
|
|
|
| actorFieldsOn::EnterpriseUserAccount => "EnterpriseUserAccount",
|
|
|
|
|
| actorFieldsOn::Mannequin => "Mannequin",
|
|
|
|
|
| actorFieldsOn::Organization => "Organization",
|
|
|
|
|
| actorFieldsOn::User => "User",
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
.into(),
|
|
|
|
|
name: login,
|
|
|
|
|
avatar_url: Some(avatar_url),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn normalize_assignee(actor: assigneeFields) -> TimelineActor {
|
|
|
|
|
match actor {
|
2026-05-18 22:30:46 +08:00
|
|
|
| assigneeFields::Bot(actor) => TimelineActor {
|
|
|
|
|
kind: "Bot".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
|
|
|
|
| assigneeFields::Mannequin(actor) => TimelineActor {
|
|
|
|
|
kind: "Mannequin".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
|
|
|
|
| assigneeFields::Organization(actor) => TimelineActor {
|
|
|
|
|
kind: "Organization".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
|
|
|
|
| assigneeFields::User(actor) => TimelineActor {
|
|
|
|
|
kind: "User".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn normalize_requested_reviewer(actor: requestedReviewerFields) -> TimelineActor {
|
|
|
|
|
match actor {
|
2026-05-18 22:30:46 +08:00
|
|
|
| requestedReviewerFields::Bot(actor) => TimelineActor {
|
|
|
|
|
kind: "Bot".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
|
|
|
|
| requestedReviewerFields::Mannequin(actor) => TimelineActor {
|
|
|
|
|
kind: "Mannequin".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
|
|
|
|
| requestedReviewerFields::Team(actor) => TimelineActor {
|
|
|
|
|
kind: "Team".into(),
|
|
|
|
|
name: actor.name,
|
|
|
|
|
avatar_url: None,
|
|
|
|
|
},
|
|
|
|
|
| requestedReviewerFields::User(actor) => TimelineActor {
|
|
|
|
|
kind: "User".into(),
|
|
|
|
|
name: actor.login,
|
|
|
|
|
avatar_url: Some(actor.avatar_url),
|
|
|
|
|
},
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn normalize_review_state(state: PullRequestReviewState) -> String {
|
|
|
|
|
match state {
|
2026-05-18 22:30:46 +08:00
|
|
|
| PullRequestReviewState::PENDING => "PENDING",
|
|
|
|
|
| PullRequestReviewState::COMMENTED => "COMMENTED",
|
|
|
|
|
| PullRequestReviewState::APPROVED => "APPROVED",
|
|
|
|
|
| PullRequestReviewState::CHANGES_REQUESTED => "CHANGES_REQUESTED",
|
|
|
|
|
| PullRequestReviewState::DISMISSED => "DISMISSED",
|
|
|
|
|
| _ => "OTHER",
|
2026-05-11 00:32:12 +08:00
|
|
|
}
|
|
|
|
|
.into()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn normalize_item(
|
|
|
|
|
value: PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes,
|
|
|
|
|
) -> PullRequestTimelineItem {
|
|
|
|
|
match value {
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AssignedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Assigned {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
assignee: event.assignee.map(normalize_assignee),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::UnassignedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Unassigned {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
assignee: event.assignee.map(normalize_assignee),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::IssueComment(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Comment {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
author: event.author.map(normalize_actor),
|
|
|
|
|
body: event.body,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::PullRequestCommit(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Commit {
|
|
|
|
|
committed_at: event.commit.committed_date,
|
|
|
|
|
abbreviated_oid: event.commit.abbreviated_oid,
|
|
|
|
|
message_headline: event.commit.message_headline,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::PullRequestReview(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Review {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
author: event.author.map(normalize_actor),
|
|
|
|
|
state: normalize_review_state(event.state),
|
|
|
|
|
body: event.body,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReviewRequestedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::ReviewRequested {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
reviewer: event.requested_reviewer.map(normalize_requested_reviewer),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReviewRequestRemovedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::ReviewRequestRemoved {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
reviewer: event.requested_reviewer.map(normalize_requested_reviewer),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReviewDismissedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::ReviewDismissed {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::MergedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Merged {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ClosedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Closed {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReopenedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Reopened {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ConvertToDraftEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::ConvertToDraft {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReadyForReviewEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::ReadyForReview {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::HeadRefForcePushedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::HeadRefForcePushed {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
before_commit_oid: event.before_commit.map(|commit| commit.abbreviated_oid),
|
|
|
|
|
after_commit_oid: event.after_commit.map(|commit| commit.abbreviated_oid),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::BaseRefChangedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::BaseRefChanged {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::LabeledEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Labeled {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
label: event.label.name,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::UnlabeledEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Unlabeled {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
label: event.label.name,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::MilestonedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Milestoned {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
milestone_title: event.milestone_title,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::DemilestonedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Demilestoned {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
milestone_title: event.milestone_title,
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::ReferencedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::Referenced {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::CrossReferencedEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::CrossReferenced {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AutoMergeEnabledEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::AutoMergeEnabled {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AutoMergeDisabledEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::AutoMergeDisabled {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
reason: event.reason.unwrap_or_default(),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::AddedToMergeQueueEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::AddedToMergeQueue {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
PullRequestTimelineQueryNodeOnPullRequestTimelineItemsNodes::RemovedFromMergeQueueEvent(
|
|
|
|
|
event,
|
|
|
|
|
) => PullRequestTimelineItem::RemovedFromMergeQueue {
|
|
|
|
|
created_at: event.created_at,
|
|
|
|
|
actor: event.actor.map(normalize_actor),
|
|
|
|
|
},
|
|
|
|
|
_ => PullRequestTimelineItem::Other {
|
|
|
|
|
typename: "Other".into(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(debug_assertions)]
|
|
|
|
|
let data = if c.should_use_fixtures {
|
|
|
|
|
super::mock::fetch_pull_request_timeline(&self.id, self.after.as_deref())?
|
|
|
|
|
} else {
|
|
|
|
|
let gql =
|
|
|
|
|
PullRequestTimelineQuery::build_query(pull_request_timeline_query::Variables {
|
|
|
|
|
id: self.id.clone().into(),
|
|
|
|
|
first: self.first,
|
|
|
|
|
after: self.after.clone(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let res = c.github_graphql_request(&gql)?.send().await?;
|
|
|
|
|
|
|
|
|
|
api::parse_graphql_response::<PullRequestTimelineResponse>(res)
|
|
|
|
|
.await?
|
|
|
|
|
.1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[cfg(not(debug_assertions))]
|
|
|
|
|
let data = {
|
|
|
|
|
let gql =
|
|
|
|
|
PullRequestTimelineQuery::build_query(pull_request_timeline_query::Variables {
|
|
|
|
|
id: self.id.clone().into(),
|
|
|
|
|
first: self.first,
|
|
|
|
|
after: self.after.clone(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let res = c.github_graphql_request(&gql)?.send().await?;
|
|
|
|
|
|
|
|
|
|
api::parse_graphql_response::<PullRequestTimelineResponse>(res)
|
|
|
|
|
.await?
|
|
|
|
|
.1
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let pull_request = data
|
|
|
|
|
.node
|
|
|
|
|
.ok_or(api::Error::MalformedResponse(
|
|
|
|
|
"missing 'node' field on PullRequestTimelineQuery response".into(),
|
|
|
|
|
))
|
|
|
|
|
.and_then(|node| match node {
|
2026-05-18 22:30:46 +08:00
|
|
|
| PullRequestTimelineQueryNode::PullRequest(pull_request) => Ok(pull_request),
|
|
|
|
|
| _ => Err(api::Error::MalformedResponse(
|
|
|
|
|
"unexpected node type on PullRequestTimelineQuery".into(),
|
|
|
|
|
)),
|
2026-05-11 00:32:12 +08:00
|
|
|
})?;
|
|
|
|
|
|
|
|
|
|
let timeline = pull_request.timeline_items;
|
|
|
|
|
let items = timeline
|
|
|
|
|
.nodes
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.flatten()
|
|
|
|
|
.map(normalize_item)
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
Ok(PullRequestTimeline {
|
|
|
|
|
items,
|
|
|
|
|
end_cursor: timeline.page_info.end_cursor,
|
|
|
|
|
has_next_page: timeline.page_info.has_next_page,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|