From 3cfd8e8855a5e62e908aaaf3697b32af639de032 Mon Sep 17 00:00:00 2001 From: hirokisan Date: Thu, 21 Nov 2024 17:33:17 +0900 Subject: [PATCH] refactor: overall --- src/github.rs | 5 +- src/github/client.rs | 879 +++++++++++----------------- src/github/gql.rs | 4 +- src/github/gql/query.rs | 2 +- src/github/pull_requests_summary.rs | 204 +++++++ src/main.rs | 16 +- 6 files changed, 549 insertions(+), 561 deletions(-) create mode 100644 src/github/pull_requests_summary.rs diff --git a/src/github.rs b/src/github.rs index 374a312..de0bbe0 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1,2 +1,3 @@ -pub mod client; -pub mod gql; +pub(crate) mod client; +pub(super) mod gql; +pub(crate) mod pull_requests_summary; diff --git a/src/github/client.rs b/src/github/client.rs index b109422..82c0226 100644 --- a/src/github/client.rs +++ b/src/github/client.rs @@ -1,8 +1,7 @@ use graphql_client::GraphQLQuery; -use serde::Serialize; use crate::github::gql::query::{pull_requests_query, PullRequestsQuery}; -use crate::github::gql::scaler::DateTime; +use crate::github::pull_requests_summary::{PullRequestSummary, PullRequestsSummary}; use std::collections::HashMap; @@ -10,180 +9,27 @@ pub struct Client { inner: octocrab::Octocrab, } -#[derive(Debug, Serialize)] -pub struct PullRequestsSummary { - start_date: String, - end_date: String, - - prs_count: i64, - comments_count: PullRequestCommentsCount, - commits_count: PullRequestCommitsCount, - changed_files_count: PullRequestChangedFilesCount, - time_to_first_contacted: PullRequestTimeToFirstContacted, - time_to_approved: PullRequestTimeToApproved, - time_to_merged: PullRequestTimeToMerged, - - prs_summaries: Vec, +struct PullRequests { + inner: Vec, } -#[derive(Debug, Serialize)] -pub struct PullRequestSummary { - url: String, - author: String, - comments_count: i64, - reviewee_comments_count: i64, - reviewer_comments_count: i64, - commits_count: i64, - changed_files_count: i64, - created_at: DateTime, - first_contacted_at: Option, - approved_at: Option, - merged_at: Option, +struct PullRequest { + inner: pull_requests_query::PullRequestsQuerySearchNodesOnPullRequest, } -#[derive(Debug, Serialize)] -pub struct PullRequestCommentsCount { - sum: i64, - average: f64, -} - -#[derive(Debug, Serialize)] -pub struct PullRequestCommitsCount { - sum: i64, - average: f64, -} - -#[derive(Debug, Serialize)] -pub struct PullRequestChangedFilesCount { - sum: i64, - average: f64, -} - -#[derive(Debug, Serialize)] -pub struct PullRequestTimeToFirstContacted { - average: f64, // sec -} - -#[derive(Debug, Serialize)] -pub struct PullRequestTimeToApproved { - average: f64, // sec -} - -#[derive(Debug, Serialize)] -pub struct PullRequestTimeToMerged { - average: f64, // sec -} - -impl PullRequestsSummary { - fn aggregate_summary(&mut self) { - self.aggregate_comments_count(); - self.aggregate_commits_count(); - self.aggregate_changed_files_count(); - self.aggregate_time_to_first_contacted(); - self.aggregate_time_to_approved(); - self.aggregate_time_to_merged(); - } - - fn aggregate_comments_count(&mut self) { - self.comments_count.sum = self - .prs_summaries - .iter() - .map(|summary| summary.comments_count) - .sum(); - self.comments_count.average = if self.prs_summaries.is_empty() { - 0.0 - } else { - self.comments_count.sum as f64 / self.prs_summaries.len() as f64 - }; - } - - fn aggregate_commits_count(&mut self) { - self.commits_count.sum = self - .prs_summaries - .iter() - .map(|summary| summary.commits_count) - .sum(); - self.commits_count.average = if self.prs_count == 0 { - 0.0 - } else { - self.commits_count.sum as f64 / self.prs_count as f64 - }; +impl PullRequest { + fn new(inner: pull_requests_query::PullRequestsQuerySearchNodesOnPullRequest) -> Self { + Self { inner } } +} - fn aggregate_changed_files_count(&mut self) { - self.changed_files_count.sum = self - .prs_summaries - .iter() - .map(|summary| summary.changed_files_count) - .sum(); - self.changed_files_count.average = if self.prs_count == 0 { - 0.0 - } else { - self.changed_files_count.sum as f64 / self.prs_count as f64 - }; +impl PullRequests { + fn new() -> Self { + Self { inner: Vec::new() } } - fn aggregate_time_to_first_contacted(&mut self) { - let mut count = 0; - let mut total_seconds = 0; - for summary in self.prs_summaries.iter() { - if summary.first_contacted_at.is_none() { - continue; - }; - count += 1; - total_seconds += summary - .first_contacted_at - .as_ref() - .unwrap() - .diff_seconds(&summary.created_at); - } - self.time_to_first_contacted.average = if count == 0 { - 0.0 - } else { - total_seconds as f64 / count as f64 - }; - } - - fn aggregate_time_to_approved(&mut self) { - let mut count = 0; - let mut total_seconds = 0; - for summary in self.prs_summaries.iter() { - if summary.approved_at.is_none() { - continue; - }; - count += 1; - total_seconds += summary - .approved_at - .as_ref() - .unwrap() - .diff_seconds(&summary.created_at); - } - self.time_to_approved.average = if count == 0 { - 0.0 - } else { - total_seconds as f64 / count as f64 - }; - } - - fn aggregate_time_to_merged(&mut self) { - let mut count = 0; - let mut total_seconds = 0; - for summary in self.prs_summaries.iter() { - if summary.merged_at.is_none() { - continue; - }; - count += 1; - total_seconds += summary - .merged_at - .as_ref() - .unwrap() - .diff_seconds(&summary.created_at); - } - self.time_to_merged.average = if count == 0 { - 0.0 - } else { - total_seconds as f64 / count as f64 - }; + fn add(&mut self, item: PullRequest) { + self.inner.push(item); } } @@ -196,12 +42,14 @@ impl Client { Self { inner: octocrab } } - pub async fn get_pull_requests_summary( + async fn get_pull_requests( &self, - repo: String, - start_date: String, - end_date: String, - ) -> PullRequestsSummary { + repo: &str, + start_date: &str, + end_date: &str, + ) -> Result { + let mut result = PullRequests::new(); + let offset = 10; let query = format!("repo:{repo} is:pull-request created:{start_date}..{end_date}"); let mut variables = pull_requests_query::Variables { @@ -211,28 +59,6 @@ impl Client { after: None, }; - let mut summary = PullRequestsSummary { - start_date, - end_date, - prs_count: 0, - comments_count: PullRequestCommentsCount { - sum: 0, - average: 0.0, - }, - commits_count: PullRequestCommitsCount { - sum: 0, - average: 0.0, - }, - changed_files_count: PullRequestChangedFilesCount { - sum: 0, - average: 0.0, - }, - time_to_first_contacted: PullRequestTimeToFirstContacted { average: 0.0 }, - time_to_approved: PullRequestTimeToApproved { average: 0.0 }, - time_to_merged: PullRequestTimeToMerged { average: 0.0 }, - prs_summaries: vec![], - }; - loop { let response: octocrab::Result< graphql_client::Response, @@ -242,163 +68,176 @@ impl Client { .await; match response { - Ok(response) => { - let prs = &response.data.as_ref().unwrap().search; - summary.prs_count = prs.issue_count; + Ok(res) => { + let prs = &res.data.as_ref().unwrap().search; + let has_next_page = prs.page_info.has_next_page; + let end_cursor = prs.page_info.end_cursor.clone(); for item in prs.nodes.as_ref().unwrap().iter().flatten() { - let pr = match item { + match item { pull_requests_query::PullRequestsQuerySearchNodes::PullRequest(pr) => { - pr + result.add(PullRequest::new(pr.clone())) } _ => continue, }; - let author = match pr.author.as_ref() { - Some(author) => author.login.clone(), - None => "no-author".to_string(), - }; + } - let reviews = pr.reviews.as_ref().unwrap().nodes.as_ref(); - let comments = pr.comments.nodes.as_ref(); - let first_review = reviews.unwrap().iter().find(|review| { - if review.as_ref().unwrap().author.as_ref().is_none() { + if !has_next_page { + break; + } + variables.after.clone_from(&end_cursor); + } + Err(err) => return Err(anyhow::anyhow!(err)), + } + } + + Ok(result) + } + + pub async fn get_pull_requests_summary( + &self, + repo: String, + start_date: String, + end_date: String, + ) -> Result { + let mut summary = PullRequestsSummary::new(start_date.clone(), end_date.clone()); + + let pull_requests = self + .get_pull_requests(&repo, &start_date, &end_date) + .await?; + + summary.prs_count = pull_requests.inner.len() as i64; + for pull_request in pull_requests.inner.iter() { + let pr = &pull_request.inner; + + let author = match pr.author.as_ref() { + Some(author) => author.login.clone(), + None => "no-author".to_string(), + }; + + let reviews = pr.reviews.as_ref().unwrap().nodes.as_ref(); + let comments = pr.comments.nodes.as_ref(); + let first_review = reviews.unwrap().iter().find(|review| { + if review.as_ref().unwrap().author.as_ref().is_none() { + return false; + } + review.as_ref().unwrap().author.as_ref().unwrap().login != author.as_ref() + }); + let first_reviewed_at = + first_review.map(|review| review.as_ref().unwrap().created_at.clone()); + let first_comment = comments.unwrap().iter().find(|comment| { + if comment.as_ref().unwrap().author.as_ref().is_none() { + return false; + } + comment.as_ref().unwrap().author.as_ref().unwrap().login != author.as_ref() + }); + let first_commented_at = + first_comment.map(|comment| comment.as_ref().unwrap().created_at.clone()); + let first_contacted_at = match (first_reviewed_at, first_commented_at) { + (None, Some(commented_at)) => Some(commented_at), + (Some(reviewed_at), None) => Some(reviewed_at), + (Some(reviewed_at), Some(commented_at)) => { + if reviewed_at > commented_at { + Some(commented_at) + } else { + Some(reviewed_at) + } + } + (None, None) => None, + }; + + let mut reviewee_comments_count = 0; + { + reviewee_comments_count += reviews + .unwrap() + .iter() + .filter(|item| match item { + Some(item) => { + if item.author.as_ref().is_none() { return false; } - review.as_ref().unwrap().author.as_ref().unwrap().login - != author.as_ref() - }); - let first_reviewed_at = - first_review.map(|review| review.as_ref().unwrap().created_at.clone()); - let first_comment = comments.unwrap().iter().find(|comment| { - if comment.as_ref().unwrap().author.as_ref().is_none() { + item.author.as_ref().unwrap().login == author.as_ref() + } + _ => false, + }) + .count() as i64; + reviewee_comments_count += comments + .unwrap() + .iter() + .filter(|item| match item { + Some(item) => { + if item.author.as_ref().is_none() { return false; } - comment.as_ref().unwrap().author.as_ref().unwrap().login - != author.as_ref() - }); - let first_commented_at = first_comment - .map(|comment| comment.as_ref().unwrap().created_at.clone()); - let first_contacted_at = match (first_reviewed_at, first_commented_at) { - (None, Some(commented_at)) => Some(commented_at), - (Some(reviewed_at), None) => Some(reviewed_at), - (Some(reviewed_at), Some(commented_at)) => { - if reviewed_at > commented_at { - Some(commented_at) - } else { - Some(reviewed_at) - } + item.author.as_ref().unwrap().login == author.as_ref() + } + _ => false, + }) + .count() as i64; + } + let mut reviewer_comments_count = 0; + { + reviewer_comments_count += reviews + .unwrap() + .iter() + .filter(|item| match item { + Some(item) => { + if item.state == pull_requests_query::PullRequestReviewState::APPROVED + && item.body.is_empty() + { + return false; } - (None, None) => None, - }; - - let mut reviewee_comments_count = 0; - { - reviewee_comments_count += reviews - .unwrap() - .iter() - .filter(|item| match item { - Some(item) => { - if item.author.as_ref().is_none() { - return false; - } - item.author.as_ref().unwrap().login == author.as_ref() - } - _ => false, - }) - .count() - as i64; - reviewee_comments_count += comments - .unwrap() - .iter() - .filter(|item| match item { - Some(item) => { - if item.author.as_ref().is_none() { - return false; - } - item.author.as_ref().unwrap().login == author.as_ref() - } - _ => false, - }) - .count() - as i64; + if item.author.as_ref().is_none() { + return false; + } + item.author.as_ref().unwrap().login != author.as_ref() } - let mut reviewer_comments_count = 0; - { - reviewer_comments_count += reviews - .unwrap() - .iter() - .filter(|item| match item { - Some(item) => { - if item.state - == pull_requests_query::PullRequestReviewState::APPROVED - && item.body.is_empty() - { - return false; - } - if item.author.as_ref().is_none() { - return false; - } - item.author.as_ref().unwrap().login != author.as_ref() - } - _ => false, - }) - .count() - as i64; - reviewer_comments_count += comments - .unwrap() - .iter() - .filter(|item| match item { - Some(item) => { - if item.author.as_ref().is_none() { - return false; - } - item.author.as_ref().unwrap().login != author.as_ref() - } - _ => false, - }) - .count() - as i64; + _ => false, + }) + .count() as i64; + reviewer_comments_count += comments + .unwrap() + .iter() + .filter(|item| match item { + Some(item) => { + if item.author.as_ref().is_none() { + return false; + } + item.author.as_ref().unwrap().login != author.as_ref() } + _ => false, + }) + .count() as i64; + } - let approved_at = reviews - .unwrap() - .iter() - .find(|review| { - review.as_ref().unwrap().state - == pull_requests_query::PullRequestReviewState::APPROVED - }) - .map(|approved| approved.as_ref().unwrap().created_at.clone()); - - let merged_at = pr.merged_at.clone(); - summary.prs_summaries.push(PullRequestSummary { - url: pr.url.clone(), - author, - comments_count: pr.total_comments_count.unwrap(), - reviewee_comments_count, - reviewer_comments_count, - commits_count: pr.commits.total_count, - changed_files_count: pr.changed_files, - created_at: pr.created_at.clone(), - first_contacted_at, - approved_at, - merged_at, - }) - } - - if !prs.page_info.has_next_page { - break; - } - variables.after.clone_from(&prs.page_info.end_cursor); - } - Err(error) => { - println!("{error:#?}"); - } - }; + let approved_at = reviews + .unwrap() + .iter() + .find(|review| { + review.as_ref().unwrap().state + == pull_requests_query::PullRequestReviewState::APPROVED + }) + .map(|approved| approved.as_ref().unwrap().created_at.clone()); + + let merged_at = pr.merged_at.clone(); + summary.prs_summaries.push(PullRequestSummary { + url: pr.url.clone(), + author, + comments_count: pr.total_comments_count.unwrap(), + reviewee_comments_count, + reviewer_comments_count, + commits_count: pr.commits.total_count, + changed_files_count: pr.changed_files, + created_at: pr.created_at.clone(), + first_contacted_at, + approved_at, + merged_at, + }) } summary.aggregate_summary(); - summary + Ok(summary) } pub async fn get_pull_requests_summary_on_individuals( @@ -407,234 +246,174 @@ impl Client { start_date: String, end_date: String, individuals: Vec, - ) -> HashMap { - let offset = 10; - let query = format!("repo:{repo} is:pull-request created:{start_date}..{end_date}"); - let mut variables = pull_requests_query::Variables { - first: offset, - query: query.to_string(), - threshold: 50, - after: None, - }; - + ) -> Result, anyhow::Error> { let mut summaries: HashMap = HashMap::new(); - loop { - let response: octocrab::Result< - graphql_client::Response, - > = self - .inner - .graphql(&PullRequestsQuery::build_query(variables.clone())) - .await; - - match response { - Ok(response) => { - let prs = &response.data.as_ref().unwrap().search; - - for individual in individuals.iter() { - for item in prs.nodes.as_ref().unwrap().iter().flatten() { - let pr = match item { - pull_requests_query::PullRequestsQuerySearchNodes::PullRequest( - pr, - ) => pr, - _ => continue, - }; - let author = match pr.author.as_ref() { - Some(author) => author.login.clone(), - None => "no-author".to_string(), - }; - summaries - .entry(individual.clone()) - .and_modify(|summary| { - if *individual == author { - summary.prs_count += 1 - } - }) - .or_insert(PullRequestsSummary { - start_date: start_date.clone(), - end_date: end_date.clone(), - prs_count: 0, - comments_count: PullRequestCommentsCount { - sum: 0, - average: 0.0, - }, - commits_count: PullRequestCommitsCount { - sum: 0, - average: 0.0, - }, - changed_files_count: PullRequestChangedFilesCount { - sum: 0, - average: 0.0, - }, - time_to_first_contacted: PullRequestTimeToFirstContacted { - average: 0.0, - }, - time_to_approved: PullRequestTimeToApproved { average: 0.0 }, - time_to_merged: PullRequestTimeToMerged { average: 0.0 }, - prs_summaries: vec![], - }); - - let reviews = pr.reviews.as_ref().unwrap().nodes.as_ref(); - let comments = pr.comments.nodes.as_ref(); - let first_review = reviews.as_ref().unwrap().iter().find(|review| { - if review.as_ref().unwrap().author.as_ref().is_none() { + let pull_requests = self + .get_pull_requests(&repo, &start_date, &end_date) + .await?; + + for pull_request in pull_requests.inner.iter() { + let pr = &pull_request.inner; + + for individual in individuals.iter() { + let author = match pr.author.as_ref() { + Some(author) => author.login.clone(), + None => "no-author".to_string(), + }; + summaries + .entry(individual.clone()) + .and_modify(|summary| { + if *individual == author { + summary.prs_count += 1 + } + }) + .or_insert(PullRequestsSummary::new( + start_date.clone(), + end_date.clone(), + )); + + let reviews = pr.reviews.as_ref().unwrap().nodes.as_ref(); + let comments = pr.comments.nodes.as_ref(); + let first_review = reviews.as_ref().unwrap().iter().find(|review| { + if review.as_ref().unwrap().author.as_ref().is_none() { + return false; + } + review.as_ref().unwrap().author.as_ref().unwrap().login != author + && review.as_ref().unwrap().author.as_ref().unwrap().login + == individual.as_ref() + }); + + let first_reviewed_at = + first_review.map(|review| review.as_ref().unwrap().created_at.clone()); + let first_comment = comments.as_ref().unwrap().iter().find(|comment| { + if comment.as_ref().unwrap().author.as_ref().is_none() { + return false; + } + comment.as_ref().unwrap().author.as_ref().unwrap().login != author + && comment.as_ref().unwrap().author.as_ref().unwrap().login + == individual.as_ref() + }); + let first_commented_at = + first_comment.map(|comment| comment.as_ref().unwrap().created_at.clone()); + let first_contacted_at = match (first_reviewed_at, first_commented_at) { + (None, Some(commented_at)) => Some(commented_at), + (Some(reviewed_at), None) => Some(reviewed_at), + (Some(reviewed_at), Some(commented_at)) => { + if reviewed_at > commented_at { + Some(commented_at) + } else { + Some(reviewed_at) + } + } + (None, None) => None, + }; + + let mut comments_count = 0; + { + comments_count += reviews + .as_ref() + .unwrap() + .iter() + .filter(|item| match item { + Some(item) => { + if item.author.as_ref().is_none() { return false; } - review.as_ref().unwrap().author.as_ref().unwrap().login != author - && review.as_ref().unwrap().author.as_ref().unwrap().login - == individual.as_ref() - }); - - let first_reviewed_at = first_review - .map(|review| review.as_ref().unwrap().created_at.clone()); - let first_comment = comments.as_ref().unwrap().iter().find(|comment| { - if comment.as_ref().unwrap().author.as_ref().is_none() { + item.author.as_ref().unwrap().login == individual.as_ref() + } + _ => false, + }) + .count() as i64; + comments_count += comments + .as_ref() + .unwrap() + .iter() + .filter(|item| match item { + Some(item) => { + if item.author.as_ref().is_none() { return false; } - comment.as_ref().unwrap().author.as_ref().unwrap().login != author - && comment.as_ref().unwrap().author.as_ref().unwrap().login - == individual.as_ref() - }); - let first_commented_at = first_comment - .map(|comment| comment.as_ref().unwrap().created_at.clone()); - let first_contacted_at = match (first_reviewed_at, first_commented_at) { - (None, Some(commented_at)) => Some(commented_at), - (Some(reviewed_at), None) => Some(reviewed_at), - (Some(reviewed_at), Some(commented_at)) => { - if reviewed_at > commented_at { - Some(commented_at) - } else { - Some(reviewed_at) - } - } - (None, None) => None, - }; - - let mut comments_count = 0; - { - comments_count += reviews - .as_ref() - .unwrap() - .iter() - .filter(|item| match item { - Some(item) => { - if item.author.as_ref().is_none() { - return false; - } - item.author.as_ref().unwrap().login - == individual.as_ref() - } - _ => false, - }) - .count() - as i64; - comments_count += comments - .as_ref() - .unwrap() - .iter() - .filter(|item| match item { - Some(item) => { - if item.author.as_ref().is_none() { - return false; - } - item.author.as_ref().unwrap().login - == individual.as_ref() - } - _ => false, - }) - .count() - as i64; + item.author.as_ref().unwrap().login == individual.as_ref() } + _ => false, + }) + .count() as i64; + } - let reviewee_comments_count = if author == individual.as_ref() { - comments_count - } else { - 0 - }; - let reviewer_comments_count = if author != individual.as_ref() { - comments_count - } else { - 0 - }; - - let commits_count = if author == *individual { - pr.commits.total_count - } else { - 0 - }; - let changed_files_count = if author == *individual { - pr.changed_files - } else { - 0 - }; - - let approved_at = - match reviews.as_ref().unwrap().iter().find(|review| { - review.as_ref().unwrap().state - == pull_requests_query::PullRequestReviewState::APPROVED - }) { - Some(approved) => { - match approved - .as_ref() - .unwrap() - .author - .as_ref() - .unwrap() - .login - .clone() - == individual.as_ref() - { - true => { - Some(approved.as_ref().unwrap().created_at.clone()) - } - false => None, - } - } - None => None, - }; - - let merged_at = match pr.merged_by.as_ref() { - Some(merged_by) => { - match merged_by.login.clone() == individual.as_ref() { - true => pr.merged_at.clone(), - false => None, - } - } - None => None, - }; - - summaries.entry(individual.clone()).and_modify(|summary| { - summary.prs_summaries.push(PullRequestSummary { - url: pr.url.clone(), - author: author.clone(), - comments_count, - reviewee_comments_count, - reviewer_comments_count, - commits_count, - changed_files_count, - created_at: pr.created_at.clone(), - first_contacted_at, - approved_at, - merged_at, - }) - }); + let reviewee_comments_count = if author == individual.as_ref() { + comments_count + } else { + 0 + }; + let reviewer_comments_count = if author != individual.as_ref() { + comments_count + } else { + 0 + }; + + let commits_count = if author == *individual { + pr.commits.total_count + } else { + 0 + }; + let changed_files_count = if author == *individual { + pr.changed_files + } else { + 0 + }; + + let approved_at = match reviews.as_ref().unwrap().iter().find(|review| { + review.as_ref().unwrap().state + == pull_requests_query::PullRequestReviewState::APPROVED + }) { + Some(approved) => { + match approved + .as_ref() + .unwrap() + .author + .as_ref() + .unwrap() + .login + .clone() + == individual.as_ref() + { + true => Some(approved.as_ref().unwrap().created_at.clone()), + false => None, } - summaries.entry(individual.clone()).and_modify(|summary| { - summary.aggregate_summary(); - }); - } - - if !prs.page_info.has_next_page { - break; } - variables.after.clone_from(&prs.page_info.end_cursor); - } - Err(error) => { - println!("{error:#?}"); - } - }; + None => None, + }; + + let merged_at = match pr.merged_by.as_ref() { + Some(merged_by) => match merged_by.login.clone() == individual.as_ref() { + true => pr.merged_at.clone(), + false => None, + }, + None => None, + }; + + summaries.entry(individual.clone()).and_modify(|summary| { + summary.prs_summaries.push(PullRequestSummary { + url: pr.url.clone(), + author: author.clone(), + comments_count, + reviewee_comments_count, + reviewer_comments_count, + commits_count, + changed_files_count, + created_at: pr.created_at.clone(), + first_contacted_at, + approved_at, + merged_at, + }) + }); + summaries.entry(individual.clone()).and_modify(|summary| { + summary.aggregate_summary(); + }); + } } - summaries + Ok(summaries) } } diff --git a/src/github/gql.rs b/src/github/gql.rs index 806f1fe..248eec0 100644 --- a/src/github/gql.rs +++ b/src/github/gql.rs @@ -1,2 +1,2 @@ -pub mod query; -pub mod scaler; +pub(super) mod query; +pub(super) mod scaler; diff --git a/src/github/gql/query.rs b/src/github/gql/query.rs index c963492..cabccbf 100644 --- a/src/github/gql/query.rs +++ b/src/github/gql/query.rs @@ -7,6 +7,6 @@ use crate::github::gql::scaler::*; schema_path = "src/github/gql/schema.json", query_path = "src/github/gql/pull_requests_query.graphql", variables_derives = "Clone, Debug, PartialEq", - response_derives = "Debug" + response_derives = "Debug, Clone" )] pub struct PullRequestsQuery; diff --git a/src/github/pull_requests_summary.rs b/src/github/pull_requests_summary.rs new file mode 100644 index 0000000..c7ba460 --- /dev/null +++ b/src/github/pull_requests_summary.rs @@ -0,0 +1,204 @@ +use crate::github::gql::scaler::DateTime; + +use serde::Serialize; + +#[derive(Debug, Serialize)] +pub(crate) struct PullRequestsSummary { + start_date: String, + end_date: String, + + pub(super) prs_count: i64, + comments_count: PullRequestCommentsCount, + commits_count: PullRequestCommitsCount, + changed_files_count: PullRequestChangedFilesCount, + time_to_first_contacted: PullRequestTimeToFirstContacted, + time_to_approved: PullRequestTimeToApproved, + time_to_merged: PullRequestTimeToMerged, + + pub(super) prs_summaries: Vec, +} + +#[derive(Debug, Serialize)] +pub(super) struct PullRequestSummary { + pub url: String, + pub author: String, + pub comments_count: i64, + pub reviewee_comments_count: i64, + pub reviewer_comments_count: i64, + pub commits_count: i64, + pub changed_files_count: i64, + pub created_at: DateTime, + pub first_contacted_at: Option, + pub approved_at: Option, + pub merged_at: Option, +} + +#[derive(Debug, Serialize)] +struct PullRequestCommentsCount { + sum: i64, + average: f64, +} + +#[derive(Debug, Serialize)] +struct PullRequestCommitsCount { + sum: i64, + average: f64, +} + +#[derive(Debug, Serialize)] +struct PullRequestChangedFilesCount { + sum: i64, + average: f64, +} + +#[derive(Debug, Serialize)] +struct PullRequestTimeToFirstContacted { + average: f64, // sec +} + +#[derive(Debug, Serialize)] +struct PullRequestTimeToApproved { + average: f64, // sec +} + +#[derive(Debug, Serialize)] +struct PullRequestTimeToMerged { + average: f64, // sec +} + +impl PullRequestsSummary { + pub(super) fn new(start_date: String, end_date: String) -> Self { + PullRequestsSummary { + start_date, + end_date, + prs_count: 0, + comments_count: PullRequestCommentsCount { + sum: 0, + average: 0.0, + }, + commits_count: PullRequestCommitsCount { + sum: 0, + average: 0.0, + }, + changed_files_count: PullRequestChangedFilesCount { + sum: 0, + average: 0.0, + }, + time_to_first_contacted: PullRequestTimeToFirstContacted { average: 0.0 }, + time_to_approved: PullRequestTimeToApproved { average: 0.0 }, + time_to_merged: PullRequestTimeToMerged { average: 0.0 }, + prs_summaries: vec![], + } + } + + pub(super) fn aggregate_summary(&mut self) { + self.aggregate_comments_count(); + self.aggregate_commits_count(); + self.aggregate_changed_files_count(); + self.aggregate_time_to_first_contacted(); + self.aggregate_time_to_approved(); + self.aggregate_time_to_merged(); + } + + fn aggregate_comments_count(&mut self) { + self.comments_count.sum = self + .prs_summaries + .iter() + .map(|summary| summary.comments_count) + .sum(); + self.comments_count.average = if self.prs_summaries.is_empty() { + 0.0 + } else { + self.comments_count.sum as f64 / self.prs_summaries.len() as f64 + }; + } + + fn aggregate_commits_count(&mut self) { + self.commits_count.sum = self + .prs_summaries + .iter() + .map(|summary| summary.commits_count) + .sum(); + self.commits_count.average = if self.prs_count == 0 { + 0.0 + } else { + self.commits_count.sum as f64 / self.prs_count as f64 + }; + } + + fn aggregate_changed_files_count(&mut self) { + self.changed_files_count.sum = self + .prs_summaries + .iter() + .map(|summary| summary.changed_files_count) + .sum(); + self.changed_files_count.average = if self.prs_count == 0 { + 0.0 + } else { + self.changed_files_count.sum as f64 / self.prs_count as f64 + }; + } + + fn aggregate_time_to_first_contacted(&mut self) { + let mut count = 0; + let mut total_seconds = 0; + for summary in self.prs_summaries.iter() { + if summary.first_contacted_at.is_none() { + continue; + }; + count += 1; + total_seconds += summary + .first_contacted_at + .as_ref() + .unwrap() + .diff_seconds(&summary.created_at); + } + self.time_to_first_contacted.average = if count == 0 { + 0.0 + } else { + total_seconds as f64 / count as f64 + }; + } + + fn aggregate_time_to_approved(&mut self) { + let mut count = 0; + let mut total_seconds = 0; + for summary in self.prs_summaries.iter() { + if summary.approved_at.is_none() { + continue; + }; + count += 1; + total_seconds += summary + .approved_at + .as_ref() + .unwrap() + .diff_seconds(&summary.created_at); + } + self.time_to_approved.average = if count == 0 { + 0.0 + } else { + total_seconds as f64 / count as f64 + }; + } + + fn aggregate_time_to_merged(&mut self) { + let mut count = 0; + let mut total_seconds = 0; + for summary in self.prs_summaries.iter() { + if summary.merged_at.is_none() { + continue; + }; + count += 1; + total_seconds += summary + .merged_at + .as_ref() + .unwrap() + .diff_seconds(&summary.created_at); + } + self.time_to_merged.average = if count == 0 { + 0.0 + } else { + total_seconds as f64 / count as f64 + }; + } +} diff --git a/src/main.rs b/src/main.rs index ec0618b..72b70b2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,8 @@ use clap::*; use crate::daterange::get_monthly_date_ranges; -use crate::github::client::{Client, PullRequestsSummary}; +use crate::github::client::Client; +use crate::github::pull_requests_summary::PullRequestsSummary; use anyhow::Result; use chrono::NaiveDate; use std::collections::HashMap; @@ -56,7 +57,10 @@ enum Period { async fn main() -> Result<()> { let cli = Cli::parse(); - let client = Client::new(std::env::var("GITHUB_TOKEN").unwrap()); + let client = Client::new( + std::env::var("GITHUB_TOKEN") + .expect("Set the GitHub PAT as an environment variable, GITHUB_TOKEN"), + ); match cli.command { SubCommand::PullRequests { @@ -75,7 +79,7 @@ async fn main() -> Result<()> { (Scope::Team, Period::All) => { let result = client .get_pull_requests_summary(repo, start_date, end_date) - .await; + .await?; println!("{}", serde_json::to_string(&result)?); } (Scope::Team, Period::Monthly) => { @@ -89,7 +93,7 @@ async fn main() -> Result<()> { start_date.to_string(), end_date.to_string(), ) - .await, + .await?, ); } println!("{}", serde_json::to_string(&result)?); @@ -102,7 +106,7 @@ async fn main() -> Result<()> { .get_pull_requests_summary_on_individuals( repo, start_date, end_date, members, ) - .await; + .await?; println!("{}", serde_json::to_string(&result)?); } (Scope::Individual, Period::Monthly) => { @@ -121,7 +125,7 @@ async fn main() -> Result<()> { end_date.to_string(), members.clone(), ) - .await, + .await?, ); } println!("{}", serde_json::to_string(&result)?);