Skip to content
This repository was archived by the owner on Nov 6, 2025. It is now read-only.

Commit d211055

Browse files
committed
Add new fields to PullRequests query
Closes #170 - Added new fields (specified in #170) to the `PullRequests` GraphQL query. - Extended `GitHubPullRequests` to store the additional data. - Implemented support for nested types like labels, comments, reviews, and commits. - Derived `Serialize`/`Deserialize` for new types to enable database storage.
1 parent 4e486b6 commit d211055

File tree

8 files changed

+731
-98
lines changed

8 files changed

+731
-98
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1111
- Tracing with a filter set by `RUST_LOG` environment variable.
1212
- Added support for passing the SSH passphrase through the `SSH_PASSPHRASE`
1313
environment variable.
14+
- Added new fields to the `PullRequests` GraphQL query and corresponding fields to
15+
the `graphql::pull_request::PullRequest` struct.
1416

1517
### Changed
1618

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
chrono = { version = "0.4", features = ["serde"] }
78
anyhow = "1"
8-
async-graphql = "7"
9+
async-graphql = { version = "7", features = ["chrono"] }
910
async-graphql-warp = "7"
1011
base64 = "0.22"
1112
bincode = "1"
12-
chrono = "0.4"
1313
clap = { version = "4", features = ["derive"] }
1414
config = { version = "0.15", features = ["toml"], default-features = false }
1515
directories = "6"

src/database.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,7 @@ impl Database {
8383
) -> Result<()> {
8484
for item in resp {
8585
let keystr: String = format!("{owner}/{name}#{}", item.number);
86-
Database::insert(
87-
&keystr,
88-
(&item.title, &item.assignees, &item.reviewers),
89-
&self.pull_request_tree,
90-
)?;
86+
Database::insert(&keystr, &item, &self.pull_request_tree)?;
9187
}
9288
Ok(())
9389
}

src/github.rs

Lines changed: 198 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
use std::{sync::Arc, time::Duration};
22

33
use anyhow::{bail, Context, Result};
4-
use chrono::Utc;
4+
use chrono::{DateTime as ChronoDateTime, Utc};
55
use graphql_client::{GraphQLQuery, QueryBody, Response as GraphQlResponse};
66
use reqwest::{Client, RequestBuilder, Response};
7-
use serde::Serialize;
7+
use serde::{Deserialize, Serialize};
88
use tokio::time;
99
use 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+
1618
const GITHUB_FETCH_SIZE: i64 = 10;
1719
const GITHUB_URL: &str = "https://api.github.com/graphql";
1820
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
1921
const 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
)]
2933
struct 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
)]
3643
struct 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

Comments
 (0)