diff --git a/src/api/pull_request.rs b/src/api/pull_request.rs index 4d540a6..1d04bc6 100644 --- a/src/api/pull_request.rs +++ b/src/api/pull_request.rs @@ -33,11 +33,11 @@ pub(crate) struct PullRequestComment { pub(crate) struct Review { pub(crate) author: String, pub(crate) state: PullRequestReviewState, - pub(crate) body: Option, + pub(crate) body: String, pub(crate) url: String, pub(crate) created_at: DateTimeUtc, pub(crate) published_at: Option, - pub(crate) submitted_at: DateTimeUtc, + pub(crate) submitted_at: Option, pub(crate) is_minimized: bool, pub(crate) comments: Vec, } @@ -61,7 +61,7 @@ pub(crate) struct PullRequest { pub(crate) repo: String, pub(crate) number: i32, pub(crate) title: String, - pub(crate) body: Option, + pub(crate) body: String, pub(crate) state: PullRequestState, pub(crate) assignees: Vec, pub(crate) created_at: DateTimeUtc, @@ -111,7 +111,7 @@ impl TryFromKeyValue for PullRequest { url: r.url, created_at: DateTimeUtc(r.created_at), published_at: r.published_at.map(DateTimeUtc), - submitted_at: DateTimeUtc(r.submitted_at), + submitted_at: r.submitted_at.map(DateTimeUtc), is_minimized: r.is_minimized, comments: r .comments @@ -184,7 +184,7 @@ impl Default for GitHubPullRequestNode { id: String::new(), number: 0, title: String::new(), - body: None, + body: String::new(), state: PullRequestState::OPEN, created_at: Timestamp::default(), updated_at: Timestamp::default(), @@ -281,7 +281,7 @@ mod tests { id: "pr-1".to_string(), number: 1, title: "pull request 1".to_string(), - body: Some(String::new()), + body: String::new(), state: PullRequestState::OPEN, created_at: "2024-01-01T00:00:00Z".parse().unwrap(), updated_at: "2024-01-01T00:00:00Z".parse().unwrap(), @@ -316,7 +316,7 @@ mod tests { id: "pr-2".to_string(), number: 2, title: "pull request 2".to_string(), - body: Some(String::new()), + body: String::new(), state: PullRequestState::OPEN, created_at: "2024-01-01T00:00:00Z".parse().unwrap(), updated_at: "2024-01-01T00:00:00Z".parse().unwrap(), @@ -398,7 +398,7 @@ mod tests { id: "pr-1".to_string(), number: 1, title: "pull request 1".to_string(), - body: Some(String::new()), + body: String::new(), state: PullRequestState::OPEN, created_at: "2024-01-01T00:00:00Z".parse().unwrap(), updated_at: "2024-01-01T00:00:00Z".parse().unwrap(), @@ -433,7 +433,7 @@ mod tests { id: "pr-2".to_string(), number: 2, title: "pull request 2".to_string(), - body: Some(String::new()), + body: String::new(), state: PullRequestState::OPEN, created_at: "2024-01-01T00:00:00Z".parse().unwrap(), updated_at: "2024-01-01T00:00:00Z".parse().unwrap(), diff --git a/src/database/pull_request.rs b/src/database/pull_request.rs index 4958a18..8537803 100644 --- a/src/database/pull_request.rs +++ b/src/database/pull_request.rs @@ -4,7 +4,17 @@ use serde::{Deserialize, Serialize}; use super::{Database, Iter}; use crate::api::pull_request::PullRequest; -use crate::outbound::pull_requests::{PullRequestReviewState, PullRequestState}; +use crate::outbound::pull_requests::{ + PullRequestReviewDecision, PullRequestReviewState, PullRequestState, + PullRequestsRepositoryPullRequestsNodes, PullRequestsRepositoryPullRequestsNodesAssignees, + PullRequestsRepositoryPullRequestsNodesAuthor, + PullRequestsRepositoryPullRequestsNodesCommentsNodesAuthor, + PullRequestsRepositoryPullRequestsNodesCommits, PullRequestsRepositoryPullRequestsNodesLabels, + PullRequestsRepositoryPullRequestsNodesReviewRequests, + PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer, + PullRequestsRepositoryPullRequestsNodesReviews, + PullRequestsRepositoryPullRequestsNodesReviewsNodesAuthor, +}; impl Database { pub(crate) fn insert_pull_requests( @@ -72,11 +82,11 @@ pub struct RepositoryNode { pub struct ReviewNode { pub(crate) author: String, pub(crate) state: PullRequestReviewState, - pub(crate) body: Option, + pub(crate) body: String, pub(crate) url: String, pub(crate) created_at: Timestamp, pub(crate) published_at: Option, - pub(crate) submitted_at: Timestamp, + pub(crate) submitted_at: Option, pub(crate) is_minimized: bool, pub(crate) comments: GitHubPRCommentConnection, } @@ -98,7 +108,7 @@ pub struct GitHubPullRequestNode { pub(crate) id: String, pub(crate) number: i32, pub(crate) title: String, - pub(crate) body: Option, + pub(crate) body: String, pub(crate) state: PullRequestState, pub(crate) created_at: Timestamp, pub(crate) updated_at: Timestamp, @@ -117,3 +127,225 @@ pub struct GitHubPullRequestNode { pub(crate) reviews: GitHubReviewConnection, pub(crate) commits: GitHubCommitConnection, } + +impl From for String { + fn from(author: PullRequestsRepositoryPullRequestsNodesAuthor) -> Self { + match author { + PullRequestsRepositoryPullRequestsNodesAuthor::User(u) => u.login, + _ => String::new(), + } + } +} + +impl From for Vec { + fn from(labels: PullRequestsRepositoryPullRequestsNodesLabels) -> Self { + labels + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|n| n.name) + .collect() + } +} + +impl From for Vec { + fn from(assignees: PullRequestsRepositoryPullRequestsNodesAssignees) -> Self { + assignees + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|u| u.login) + .collect() + } +} + +impl From for Vec { + fn from(reqs: PullRequestsRepositoryPullRequestsNodesReviewRequests) -> Self { + reqs + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .filter_map(|rr| match rr.requested_reviewer { + Some( + PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User(u), + ) => Some(u.login), + _ => None, + }) + .collect() + } +} + +impl TryFrom for GitHubReviewConnection { + type Error = anyhow::Error; + + fn try_from(reviews: PullRequestsRepositoryPullRequestsNodesReviews) -> Result { + let total_count: i32 = reviews.total_count.try_into()?; + let nodes = reviews + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|node| ReviewNode { + author: node + .author + .and_then(|a| match a { + PullRequestsRepositoryPullRequestsNodesReviewsNodesAuthor::User(u) => { + Some(u.login) + } + _ => None, + }) + .unwrap_or_default(), + state: node.state, + body: node.body, + url: node.url, + created_at: node.created_at, + published_at: node.published_at, + submitted_at: node.submitted_at, + is_minimized: node.is_minimized, + comments: GitHubPRCommentConnection { + total_count: node.comments.total_count.try_into().unwrap_or_default(), + // FIX: assign real data, it already exists + nodes: vec![], + }, + }) + .collect(); + + Ok(Self { total_count, nodes }) + } +} + +impl TryFrom for GitHubCommitConnection { + type Error = anyhow::Error; + + fn try_from(commits: PullRequestsRepositoryPullRequestsNodesCommits) -> Result { + let total_count: i32 = commits.total_count.try_into()?; + let nodes = commits + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|node| { + let commit = node.commit; + Ok(CommitInner { + additions: commit.additions.try_into()?, + deletions: commit.deletions.try_into()?, + message: commit.message, + message_body: Some(commit.message_body), + author: commit + .author + .and_then(|a| a.user) + .map(|u| u.login) + .unwrap_or_default(), + changed_files_if_available: commit + .changed_files_if_available + .map(TryInto::try_into) + .transpose()?, + committed_date: commit.committed_date, + committer: commit + .committer + .and_then(|c| c.user) + .map(|u| u.login) + .unwrap_or_default(), + }) + }) + .collect::>>()?; + + Ok(Self { total_count, nodes }) + } +} + +impl TryFrom for GitHubPullRequestNode { + type Error = anyhow::Error; + + fn try_from(pr: PullRequestsRepositoryPullRequestsNodes) -> Result { + let number: i32 = pr.number.try_into()?; + let author = pr.author.map(String::from).unwrap_or_default(); + let additions: i32 = pr.additions.try_into()?; + let deletions: i32 = pr.deletions.try_into()?; + + let labels = pr.labels.map(Vec::::from).unwrap_or_default(); + let assignees = Vec::::from(pr.assignees); + let review_requests = pr + .review_requests + .map(Vec::::from) + .unwrap_or_default(); + + let comments_total: i32 = pr.comments.total_count.try_into()?; + let repo_owner = pr.repository.owner.login.clone(); + let repo_name = pr.repository.name.clone(); + let comments_nodes = pr + .comments + .nodes + .unwrap_or_default() + .into_iter() + .flatten() + .map(|node| GitHubPRComment { + author: match node.author { + Some(PullRequestsRepositoryPullRequestsNodesCommentsNodesAuthor::User(u)) => { + u.login + } + _ => String::new(), + }, + body: node.body, + created_at: node.created_at, + updated_at: node.updated_at, + repository_name: repo_name.clone(), + // FIX: add url field to comments query and assign it here + url: String::new(), + }) + .collect(); + + let reviews = pr + .reviews + .map(GitHubReviewConnection::try_from) + .transpose()? + .unwrap_or_else(|| GitHubReviewConnection { + total_count: 0, + nodes: vec![], + }); + + let commits = GitHubCommitConnection::try_from(pr.commits)?; + + let review_decision = pr.review_decision.and_then(|d| match d { + PullRequestReviewDecision::APPROVED => Some(PullRequestReviewState::APPROVED), + PullRequestReviewDecision::CHANGES_REQUESTED => { + Some(PullRequestReviewState::CHANGES_REQUESTED) + } + PullRequestReviewDecision::REVIEW_REQUIRED => Some(PullRequestReviewState::PENDING), + PullRequestReviewDecision::Other(_) => None, + }); + + Ok(Self { + id: pr.id, + number, + title: pr.title, + body: pr.body, + state: pr.state, + created_at: pr.created_at, + updated_at: pr.updated_at, + closed_at: pr.closed_at, + merged_at: pr.merged_at, + author, + additions, + deletions, + url: pr.url, + repository: RepositoryNode { + owner: repo_owner, + name: repo_name, + }, + labels, + comments: GitHubPRCommentConnection { + total_count: comments_total, + nodes: comments_nodes, + }, + review_decision, + assignees, + review_requests, + reviews, + commits, + }) + } +} diff --git a/src/outbound.rs b/src/outbound.rs index b0962c9..d8f5325 100644 --- a/src/outbound.rs +++ b/src/outbound.rs @@ -10,23 +10,8 @@ use tracing::error; use crate::database::DiscussionDbSchema; use crate::{ - database::{ - issue::GitHubIssue, - pull_request::{ - CommitInner, GitHubCommitConnection, GitHubPRComment, GitHubPRCommentConnection, - GitHubPullRequestNode, GitHubReviewConnection, RepositoryNode, ReviewNode, - }, - Database, - }, - outbound::{ - issues::IssueState, - pull_requests::{ - PullRequestReviewDecision, PullRequestReviewState, - PullRequestsRepositoryPullRequestsNodesAuthor::User as PullRequestAuthorUser, - PullRequestsRepositoryPullRequestsNodesCommentsNodesAuthor as PRCommentAuthor, - PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User as PRReviewRequestedUser, - }, - }, + database::{issue::GitHubIssue, pull_request::GitHubPullRequestNode, Database}, + outbound::issues::IssueState, settings::Repository as RepoInfo, }; @@ -182,7 +167,7 @@ async fn send_github_issue_query( Ok(total_issue) } -#[allow(clippy::too_many_lines)] + async fn send_github_pr_query( owner: &str, name: &str, @@ -202,181 +187,36 @@ async fn send_github_pr_query( let resp_body: GraphQlResponse = send_query::(token, var).await?.json().await?; + + // TODO: Use `let` chain instead of nested `if let Some` after migrating to Rust 2024 if let Some(data) = resp_body.data { - if let Some(repository) = data.repository { - if let Some(nodes) = repository.pull_requests.nodes { - for pr in nodes.into_iter().flatten() { - let mut assignees_list = Vec::new(); - if let Some(ass_nodes) = pr.assignees.nodes { - for node in ass_nodes.into_iter().flatten() { - assignees_list.push(node.login); - } - } - let mut rr_nodes = Vec::new(); - if let Some(req_conn) = pr.review_requests { - if let Some(req_nodes) = req_conn.nodes { - for rr in req_nodes.into_iter().flatten() { - if let Some(PRReviewRequestedUser(user_node)) = - rr.requested_reviewer - { - rr_nodes.push(user_node.login); - } + if let Some(repo) = data.repository { + if let Some(nodes) = repo.pull_requests.nodes { + { + let mut dropped = 0usize; + prs.extend(nodes.into_iter().flatten().filter_map(|n| { + match GitHubPullRequestNode::try_from(n) { + Ok(pr) => Some(pr), + Err(e) => { + tracing::warn!("Dropping PR node due to conversion error: {e}"); + dropped += 1; + None } } + })); + if dropped > 0 { + tracing::debug!("Dropped {dropped} PR nodes from this page"); } - prs.push(GitHubPullRequestNode { - id: pr.id, - number: pr.number.try_into().unwrap_or_default(), - title: pr.title, - body: Some(pr.body), - state: pr.state, - created_at: pr.created_at, - updated_at: pr.updated_at, - closed_at: pr.closed_at, - merged_at: pr.merged_at, - author: match pr.author { - Some(PullRequestAuthorUser(user)) => user.login, - _ => String::new(), - }, - additions: pr.additions.try_into().unwrap_or_default(), - deletions: pr.deletions.try_into().unwrap_or_default(), - url: pr.url, - repository: RepositoryNode { - owner: pr.repository.owner.login, - name: pr.repository.name.clone(), - }, - labels: pr - .labels - .as_ref() - .and_then(|conn| conn.nodes.as_ref()) - .map(|nodes| { - nodes - .iter() - .filter_map(|n| n.as_ref().map(|node| node.name.clone())) - .collect::>() - }) - .unwrap_or_default(), - comments: GitHubPRCommentConnection { - total_count: pr.comments.total_count.try_into().unwrap_or_default(), - nodes: pr - .comments - .nodes - .as_ref() - .into_iter() - .flatten() - .filter_map(|n| n.as_ref()) - .map(|node| GitHubPRComment { - author: match &node.author { - Some(PRCommentAuthor::User(u)) => u.login.clone(), - _ => String::new(), - }, - body: node.body.clone(), - created_at: node.created_at, - updated_at: node.updated_at, - repository_name: pr.repository.name.clone(), - url: String::new(), - }) - .collect(), - }, - - review_decision: pr.review_decision.and_then(|d| match d { - PullRequestReviewDecision::APPROVED => Some(PullRequestReviewState::APPROVED), - PullRequestReviewDecision::CHANGES_REQUESTED => Some(PullRequestReviewState::CHANGES_REQUESTED), - PullRequestReviewDecision::REVIEW_REQUIRED => Some(PullRequestReviewState::PENDING), - PullRequestReviewDecision::Other(_) => None, - }), - assignees: assignees_list, - review_requests: rr_nodes, - reviews: GitHubReviewConnection { - total_count: pr - .reviews - .as_ref() - .map(|r| r.total_count.try_into().unwrap_or_default()) - .unwrap_or_default(), - nodes: pr - .reviews - .as_ref() - .and_then(|r| r.nodes.as_ref()) - .map(|nodes| { - nodes - .iter() - .filter_map(|n| n.as_ref()) - .map(|node| ReviewNode { - author: node.author.as_ref().and_then(|a| match a { - pull_requests::PullRequestsRepositoryPullRequestsNodesReviewsNodesAuthor::User(u) => Some(u.login.clone()), - _ => None, - }).unwrap_or_default(), - state: node.state.clone(), - body: Some(node.body.clone()), - url: node.url.clone(), - created_at: node.created_at, - published_at: node.published_at, - submitted_at: node.submitted_at.unwrap_or_else(Timestamp::now), - is_minimized: node.is_minimized, - comments: GitHubPRCommentConnection { - total_count: node.comments.total_count.try_into().unwrap_or_default(), - nodes: vec![], - }, - }) - .collect() - }) - .unwrap_or_default(), - }, - commits: GitHubCommitConnection { - total_count: pr - .commits - .total_count - .try_into() - .unwrap_or_default(), - nodes: pr - .commits - .nodes - .as_ref() - .map_or(vec![], |nodes| { - nodes - .iter() - .filter_map(|n| n.as_ref()) - .map(|node| { - let commit = &node.commit; - CommitInner { - additions: commit.additions.try_into().unwrap_or_default(), - deletions: commit.deletions.try_into().unwrap_or_default(), - message: commit.message.clone(), - message_body: Some(commit.message_body.clone()), - author: commit - .author - .as_ref() - .and_then(|a| a.user.as_ref()).map(|u| u.login.clone()) - .unwrap_or_default(), - changed_files_if_available: commit - .changed_files_if_available - .and_then(|v| v.try_into().ok()), - committed_date: commit.committed_date, - committer: commit - .committer - .as_ref() - .and_then(|c| c.user.as_ref()) - .map(|user| user.login.clone()) - .unwrap_or_default(), - - } - }) - .collect() - }), - } - }); - } - if !repository.pull_requests.page_info.has_next_page { - break; + + if !repo.pull_requests.page_info.has_next_page { + break; + } + + end_cur = repo.pull_requests.page_info.end_cursor; } - end_cur = repository.pull_requests.page_info.end_cursor; - continue; } - end_cur = repository.pull_requests.page_info.end_cursor; - continue; } } - bail!("Failed to parse response data"); } Ok(prs) }