11use std:: { sync:: Arc , time:: Duration } ;
22
33use anyhow:: { bail, Context , Result } ;
4- use chrono:: Utc ;
4+ use chrono:: { DateTime as ChronoDateTime , Utc } ;
55use graphql_client:: { GraphQLQuery , QueryBody , Response as GraphQlResponse } ;
66use reqwest:: { Client , RequestBuilder , Response } ;
7- use serde:: Serialize ;
7+ use serde:: { Deserialize , Serialize } ;
88use tokio:: time;
99use tracing:: error;
1010
@@ -13,45 +13,176 @@ use crate::{database::Database, github::{
1313 pull_requests:: PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer :: User ,
1414} , settings:: Repository as RepoInfo } ;
1515
16+ pub ( crate ) use pull_requests:: { PullRequestState , PullRequestReviewState } ;
17+
1618const GITHUB_FETCH_SIZE : i64 = 10 ;
1719const GITHUB_URL : & str = "https://api.github.com/graphql" ;
1820const APP_USER_AGENT : & str = concat ! ( env!( "CARGO_PKG_NAME" ) , "/" , env!( "CARGO_PKG_VERSION" ) , ) ;
1921const INIT_TIME : & str = "1992-06-05T00:00:00Z" ;
2022
21- type DateTime = String ;
22-
23+ type DateTime = ChronoDateTime < Utc > ;
24+ #[ allow( clippy:: upper_case_acronyms) ]
25+ type URI = String ;
2326#[ derive( GraphQLQuery ) ]
2427#[ graphql(
2528 schema_path = "src/github/schema.graphql" ,
2629 query_path = "src/github/issues.graphql" ,
27- response_derives = "Debug"
30+ response_derives = "Debug" ,
31+ scalar = "DateTime = ChronoDateTime<Utc>"
2832) ]
2933struct Issues ;
3034
3135#[ derive( GraphQLQuery ) ]
3236#[ graphql(
3337 schema_path = "src/github/schema.graphql" ,
34- query_path = "src/github/pull_requests.graphql"
38+ query_path = "src/github/pull_requests.graphql" ,
39+ response_derives = "Debug, Clone, PartialEq, Eq" ,
40+ scalar = "DateTime = ChronoDateTime<Utc>" ,
41+ scalar = "URI = URI"
3542) ]
3643struct PullRequests ;
3744
3845#[ derive( Debug ) ]
39- pub ( super ) struct GitHubIssue {
40- pub ( super ) number : i64 ,
41- pub ( super ) title : String ,
42- pub ( super ) author : String ,
43- pub ( super ) closed_at : Option < DateTime > ,
46+ pub struct GitHubIssue {
47+ pub number : i64 ,
48+ pub title : String ,
49+ pub author : String ,
50+ pub closed_at : Option < ChronoDateTime < Utc > > ,
4451}
4552
46- #[ derive( Debug ) ]
47- pub ( super ) struct GitHubPullRequests {
48- pub ( super ) number : i64 ,
49- pub ( super ) title : String ,
50- pub ( super ) assignees : Vec < String > ,
51- pub ( super ) reviewers : Vec < String > ,
53+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
54+ pub struct RepositoryNode {
55+ pub owner : String ,
56+ pub name : String ,
57+ }
58+
59+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
60+ pub struct GitHubUserConnection {
61+ pub nodes : Vec < String > ,
62+ }
63+
64+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
65+ pub struct LabelNode {
66+ pub name : String ,
67+ }
68+
69+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
70+ pub struct GitHubLabelConnection {
71+ pub nodes : Vec < LabelNode > ,
72+ }
73+
74+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
75+ pub struct CommentNode {
76+ pub body : String ,
77+ #[ serde( rename = "createdAt" ) ]
78+ pub created_at : ChronoDateTime < Utc > ,
79+ #[ serde( rename = "updatedAt" ) ]
80+ pub updated_at : ChronoDateTime < Utc > ,
81+ pub author : Option < String > ,
5282}
5383
54- pub ( super ) async fn fetch_periodically (
84+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
85+ pub struct GitHubCommentConnection {
86+ #[ serde( rename = "totalCount" ) ]
87+ pub total_count : i32 ,
88+ pub nodes : Vec < CommentNode > ,
89+ }
90+
91+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
92+ pub struct ReviewNode {
93+ pub author : Option < String > ,
94+ pub state : PullRequestReviewState ,
95+ pub body : Option < String > ,
96+ pub url : String ,
97+ #[ serde( rename = "createdAt" ) ]
98+ pub created_at : ChronoDateTime < Utc > ,
99+ #[ serde( rename = "publishedAt" ) ]
100+ pub published_at : Option < ChronoDateTime < Utc > > ,
101+ #[ serde( rename = "submittedAt" ) ]
102+ pub submitted_at : Option < ChronoDateTime < Utc > > ,
103+ #[ serde( rename = "isMinimized" ) ]
104+ pub is_minimized : bool ,
105+ pub comments : GitHubCommentConnection ,
106+ }
107+
108+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
109+ pub struct GitHubReviewConnection {
110+ #[ serde( rename = "totalCount" ) ]
111+ pub total_count : i32 ,
112+ pub nodes : Vec < ReviewNode > ,
113+ }
114+
115+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
116+ pub struct ReviewRequestNode {
117+ #[ serde( rename = "requestedReviewer" ) ]
118+ pub requested_reviewer : Option < String > ,
119+ }
120+
121+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
122+ pub struct GitHubReviewRequestConnection {
123+ pub nodes : Vec < ReviewRequestNode > ,
124+ }
125+
126+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
127+ pub struct CommitPerson {
128+ pub user : Option < String > ,
129+ }
130+
131+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
132+ pub struct CommitInner {
133+ pub additions : i32 ,
134+ pub deletions : i32 ,
135+ pub message : String ,
136+ #[ serde( rename = "messageBody" ) ]
137+ pub message_body : Option < String > ,
138+ pub author : Option < CommitPerson > ,
139+ #[ serde( rename = "changedFilesIfAvailable" ) ]
140+ pub changed_files_if_available : Option < i32 > ,
141+ #[ serde( rename = "committedDate" ) ]
142+ pub committed_date : ChronoDateTime < Utc > ,
143+ pub committer : Option < CommitPerson > ,
144+ }
145+
146+ #[ derive( Debug , Clone , Serialize , Deserialize ) ]
147+ pub struct GitHubCommitConnection {
148+ #[ serde( rename = "totalCount" ) ]
149+ pub total_count : i32 ,
150+ #[ serde( rename = "nodes" ) ]
151+ pub nodes : Vec < CommitInner > ,
152+ }
153+
154+ #[ derive( Clone , Debug , Serialize , Deserialize ) ]
155+ pub struct GitHubPullRequests {
156+ pub id : String ,
157+ pub number : i32 ,
158+ pub title : String ,
159+ pub body : Option < String > ,
160+ pub state : PullRequestState ,
161+ #[ serde( rename = "createdAt" ) ]
162+ pub created_at : ChronoDateTime < Utc > ,
163+ #[ serde( rename = "updatedAt" ) ]
164+ pub updated_at : ChronoDateTime < Utc > ,
165+ #[ serde( rename = "closedAt" ) ]
166+ pub closed_at : Option < ChronoDateTime < Utc > > ,
167+ #[ serde( rename = "mergedAt" ) ]
168+ pub merged_at : Option < ChronoDateTime < Utc > > ,
169+ pub author : Option < String > ,
170+ pub additions : i32 ,
171+ pub deletions : i32 ,
172+ pub url : String ,
173+ pub repository : RepositoryNode ,
174+ pub labels : GitHubLabelConnection ,
175+ pub comments : GitHubCommentConnection ,
176+ #[ serde( rename = "reviewDecision" ) ]
177+ pub review_decision : Option < PullRequestReviewState > ,
178+ pub assignees : GitHubUserConnection ,
179+ #[ serde( rename = "reviewRequests" ) ]
180+ pub review_requests : GitHubReviewRequestConnection ,
181+ pub reviews : GitHubReviewConnection ,
182+ pub commits : GitHubCommitConnection ,
183+ }
184+
185+ pub async fn fetch_periodically (
55186 repositories : Arc < Vec < RepoInfo > > ,
56187 token : String ,
57188 period : Duration ,
@@ -126,14 +257,17 @@ async fn send_github_issue_query(
126257 let mut end_cur: Option < String > = None ;
127258 let mut issues: Vec < GitHubIssue > = Vec :: new ( ) ;
128259 loop {
260+ let since_dt = since
261+ . parse :: < ChronoDateTime < Utc > > ( )
262+ . context ( "Failed to parse since date" ) ?;
129263 let var = issues:: Variables {
130264 owner : owner. to_string ( ) ,
131265 name : name. to_string ( ) ,
132266 first : Some ( GITHUB_FETCH_SIZE ) ,
133267 last : None ,
134268 before : None ,
135269 after : end_cur,
136- since : Some ( since . to_string ( ) ) ,
270+ since : Some ( since_dt ) ,
137271 } ;
138272 let resp_body: GraphQlResponse < issues:: ResponseData > =
139273 send_query :: < Issues > ( token, var) . await ?. json ( ) . await ?;
@@ -151,7 +285,7 @@ async fn send_github_issue_query(
151285 number : issue. number ,
152286 title : issue. title . to_string ( ) ,
153287 author,
154- closed_at : issue. closed_at . clone ( ) ,
288+ closed_at : issue. closed_at ,
155289 } ) ;
156290 }
157291 if !repository. issues . page_info . has_next_page {
@@ -175,63 +309,75 @@ async fn send_github_pr_query(
175309) -> Result < Vec < Vec < GitHubPullRequests > > > {
176310 let mut total_prs = Vec :: new ( ) ;
177311 let mut end_cur: Option < String > = None ;
178- let mut prs : Vec < GitHubPullRequests > = Vec :: new ( ) ;
312+
179313 loop {
180314 let var = pull_requests:: Variables {
181315 owner : owner. to_string ( ) ,
182316 name : name. to_string ( ) ,
183317 first : Some ( GITHUB_FETCH_SIZE ) ,
184318 last : None ,
185319 before : None ,
186- after : end_cur,
320+ after : end_cur. take ( ) ,
187321 } ;
188322
189323 let resp_body: GraphQlResponse < pull_requests:: ResponseData > =
190324 send_query :: < PullRequests > ( token, var) . await ?. json ( ) . await ?;
325+
191326 if let Some ( data) = resp_body. data {
192- if let Some ( repository ) = data. repository {
193- if let Some ( nodes ) = repository . pull_requests . nodes . as_ref ( ) {
194- for pr in nodes . iter ( ) . flatten ( ) {
195- let mut assignees : Vec < String > = Vec :: new ( ) ;
196- let mut reviewers : Vec < String > = Vec :: new ( ) ;
197-
198- if let Some ( assignees_nodes ) = pr. assignees . nodes . as_ref ( ) {
199- for pr_assignees in assignees_nodes . iter ( ) . flatten ( ) {
200- assignees . push ( pr_assignees . login . clone ( ) ) ;
327+ if let Some ( repo ) = data. repository {
328+ let mut batch = Vec :: new ( ) ;
329+
330+ if let Some ( nodes ) = repo . pull_requests . nodes {
331+ for pr in nodes . into_iter ( ) . flatten ( ) {
332+ let mut assignees_list = Vec :: new ( ) ;
333+ if let Some ( ass_nodes ) = pr. assignees . nodes {
334+ for node in ass_nodes . into_iter ( ) . flatten ( ) {
335+ assignees_list . push ( node . login ) ;
201336 }
202337 }
203- if let Some ( reviewers_nodes) =
204- pr. review_requests . as_ref ( ) . and_then ( |r| r. nodes . as_ref ( ) )
205- {
206- for pr_reviewers in reviewers_nodes. iter ( ) . flatten ( ) {
207- if let Some ( User ( on_user) ) =
208- pr_reviewers. requested_reviewer . as_ref ( )
209- {
210- reviewers. push ( on_user. login . clone ( ) ) ;
338+ let assignees_conn = GitHubUserConnection {
339+ nodes : assignees_list,
340+ } ;
341+
342+ let mut rr_nodes = Vec :: new ( ) ;
343+ if let Some ( req_conn) = pr. review_requests {
344+ if let Some ( req_nodes) = req_conn. nodes {
345+ for rr in req_nodes. into_iter ( ) . flatten ( ) {
346+ if let Some ( User ( user_node) ) = rr. requested_reviewer {
347+ rr_nodes. push ( ReviewRequestNode {
348+ requested_reviewer : Some ( user_node. login ) ,
349+ } ) ;
350+ }
211351 }
212352 }
213353 }
354+ let requests_conn = GitHubReviewRequestConnection { nodes : rr_nodes } ;
214355
215- prs . push ( GitHubPullRequests {
216- number : pr. number ,
217- title : pr . title . to_string ( ) ,
218- assignees ,
219- reviewers ,
220- } ) ;
221- }
222- if !repository . pull_requests . page_info . has_next_page {
223- total_prs . push ( prs ) ;
224- break ;
356+ let record = GitHubPullRequests {
357+ number : i32 :: try_from ( pr. number )
358+ . context ( "pull request number out of i32 range" ) ? ,
359+ title : pr . title ,
360+ state : pr . state . clone ( ) ,
361+ assignees : assignees_conn ,
362+ review_requests : requests_conn ,
363+ .. Default :: default ( )
364+ } ;
365+ batch . push ( record ) ;
225366 }
226- end_cur = repository. pull_requests . page_info . end_cursor ;
227- continue ;
228367 }
229- end_cur = repository. pull_requests . page_info . end_cursor ;
368+
369+ total_prs. push ( batch) ;
370+ if !repo. pull_requests . page_info . has_next_page {
371+ break ;
372+ }
373+ end_cur = repo. pull_requests . page_info . end_cursor ;
230374 continue ;
231375 }
232376 }
377+
233378 bail ! ( "Failed to parse response data" ) ;
234379 }
380+
235381 Ok ( total_prs)
236382}
237383
0 commit comments