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

Commit 16f2b8c

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 16f2b8c

File tree

8 files changed

+732
-96
lines changed

8 files changed

+732
-96
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: 197 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use anyhow::{bail, Context, Result};
44
use chrono::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,177 @@ 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;
23+
pub(crate) type DateTime = chrono::DateTime<Utc>;
2224

25+
#[allow(clippy::upper_case_acronyms)]
26+
type URI = String;
2327
#[derive(GraphQLQuery)]
2428
#[graphql(
2529
schema_path = "src/github/schema.graphql",
2630
query_path = "src/github/issues.graphql",
27-
response_derives = "Debug"
31+
response_derives = "Debug",
32+
scalar = "DateTime = DateTime"
2833
)]
2934
struct Issues;
3035

3136
#[derive(GraphQLQuery)]
3237
#[graphql(
3338
schema_path = "src/github/schema.graphql",
34-
query_path = "src/github/pull_requests.graphql"
39+
query_path = "src/github/pull_requests.graphql",
40+
response_derives = "Debug, Clone, PartialEq, Eq",
41+
scalar = "DateTime = DateTime",
42+
scalar = "URI = URI"
3543
)]
3644
struct PullRequests;
3745

3846
#[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>,
47+
pub struct GitHubIssue {
48+
pub number: i64,
49+
pub title: String,
50+
pub author: String,
51+
pub closed_at: Option<DateTime>,
4452
}
4553

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>,
54+
#[derive(Debug, Clone, Serialize, Deserialize)]
55+
pub struct RepositoryNode {
56+
pub owner: String,
57+
pub name: String,
58+
}
59+
60+
#[derive(Debug, Clone, Serialize, Deserialize)]
61+
pub struct GitHubUserConnection {
62+
pub nodes: Vec<String>,
63+
}
64+
65+
#[derive(Debug, Clone, Serialize, Deserialize)]
66+
pub struct LabelNode {
67+
pub name: String,
68+
}
69+
70+
#[derive(Debug, Clone, Serialize, Deserialize)]
71+
pub struct GitHubLabelConnection {
72+
pub nodes: Vec<LabelNode>,
73+
}
74+
75+
#[derive(Debug, Clone, Serialize, Deserialize)]
76+
pub struct CommentNode {
77+
pub body: String,
78+
#[serde(rename = "createdAt")]
79+
pub created_at: DateTime,
80+
#[serde(rename = "updatedAt")]
81+
pub updated_at: DateTime,
82+
pub author: Option<String>,
83+
}
84+
85+
#[derive(Debug, Clone, Serialize, Deserialize)]
86+
pub struct GitHubCommentConnection {
87+
#[serde(rename = "totalCount")]
88+
pub total_count: i32,
89+
pub nodes: Vec<CommentNode>,
90+
}
91+
92+
#[derive(Debug, Clone, Serialize, Deserialize)]
93+
pub struct ReviewNode {
94+
pub author: Option<String>,
95+
pub state: PullRequestReviewState,
96+
pub body: Option<String>,
97+
pub url: String,
98+
#[serde(rename = "createdAt")]
99+
pub created_at: DateTime,
100+
#[serde(rename = "publishedAt")]
101+
pub published_at: DateTime,
102+
#[serde(rename = "submittedAt")]
103+
pub submitted_at: DateTime,
104+
#[serde(rename = "isMinimized")]
105+
pub is_minimized: bool,
106+
pub comments: GitHubCommentConnection,
107+
}
108+
109+
#[derive(Debug, Clone, Serialize, Deserialize)]
110+
pub struct GitHubReviewConnection {
111+
#[serde(rename = "totalCount")]
112+
pub total_count: i32,
113+
pub nodes: Vec<ReviewNode>,
114+
}
115+
116+
#[derive(Debug, Clone, Serialize, Deserialize)]
117+
pub struct ReviewRequestNode {
118+
#[serde(rename = "requestedReviewer")]
119+
pub requested_reviewer: Option<String>,
120+
}
121+
122+
#[derive(Debug, Clone, Serialize, Deserialize)]
123+
pub struct GitHubReviewRequestConnection {
124+
pub nodes: Vec<ReviewRequestNode>,
125+
}
126+
127+
#[derive(Debug, Clone, Serialize, Deserialize)]
128+
pub struct CommitPerson {
129+
pub user: Option<String>,
130+
}
131+
132+
#[derive(Debug, Clone, Serialize, Deserialize)]
133+
pub struct CommitInner {
134+
pub additions: i32,
135+
pub deletions: i32,
136+
pub message: String,
137+
#[serde(rename = "messageBody")]
138+
pub message_body: Option<String>,
139+
pub author: Option<CommitPerson>,
140+
#[serde(rename = "changedFilesIfAvailable")]
141+
pub changed_files_if_available: Option<i32>,
142+
#[serde(rename = "committedDate")]
143+
pub committed_date: DateTime,
144+
pub committer: Option<CommitPerson>,
52145
}
53146

54-
pub(super) async fn fetch_periodically(
147+
#[derive(Debug, Clone, Serialize, Deserialize)]
148+
pub struct GitHubCommitConnection {
149+
#[serde(rename = "totalCount")]
150+
pub total_count: i32,
151+
#[serde(rename = "nodes")]
152+
pub nodes: Vec<CommitInner>,
153+
}
154+
155+
#[derive(Clone, Debug, Serialize, Deserialize)]
156+
pub struct GitHubPullRequests {
157+
pub id: String,
158+
pub number: i32,
159+
pub title: String,
160+
pub body: Option<String>,
161+
pub state: PullRequestState,
162+
#[serde(rename = "createdAt")]
163+
pub created_at: DateTime,
164+
#[serde(rename = "updatedAt")]
165+
pub updated_at: DateTime,
166+
#[serde(rename = "closedAt")]
167+
pub closed_at: Option<DateTime>,
168+
#[serde(rename = "mergedAt")]
169+
pub merged_at: Option<DateTime>,
170+
pub author: Option<String>,
171+
pub additions: i32,
172+
pub deletions: i32,
173+
pub url: String,
174+
pub repository: RepositoryNode,
175+
pub labels: GitHubLabelConnection,
176+
pub comments: GitHubCommentConnection,
177+
#[serde(rename = "reviewDecision")]
178+
pub review_decision: Option<PullRequestReviewState>,
179+
pub assignees: GitHubUserConnection,
180+
#[serde(rename = "reviewRequests")]
181+
pub review_requests: GitHubReviewRequestConnection,
182+
pub reviews: GitHubReviewConnection,
183+
pub commits: GitHubCommitConnection,
184+
}
185+
186+
pub async fn fetch_periodically(
55187
repositories: Arc<Vec<RepoInfo>>,
56188
token: String,
57189
period: Duration,
@@ -126,14 +258,17 @@ async fn send_github_issue_query(
126258
let mut end_cur: Option<String> = None;
127259
let mut issues: Vec<GitHubIssue> = Vec::new();
128260
loop {
261+
let since_dt = since
262+
.parse::<DateTime>()
263+
.context("Failed to parse since date")?;
129264
let var = issues::Variables {
130265
owner: owner.to_string(),
131266
name: name.to_string(),
132267
first: Some(GITHUB_FETCH_SIZE),
133268
last: None,
134269
before: None,
135270
after: end_cur,
136-
since: Some(since.to_string()),
271+
since: Some(since_dt),
137272
};
138273
let resp_body: GraphQlResponse<issues::ResponseData> =
139274
send_query::<Issues>(token, var).await?.json().await?;
@@ -151,7 +286,7 @@ async fn send_github_issue_query(
151286
number: issue.number,
152287
title: issue.title.to_string(),
153288
author,
154-
closed_at: issue.closed_at.clone(),
289+
closed_at: issue.closed_at,
155290
});
156291
}
157292
if !repository.issues.page_info.has_next_page {
@@ -175,63 +310,75 @@ async fn send_github_pr_query(
175310
) -> Result<Vec<Vec<GitHubPullRequests>>> {
176311
let mut total_prs = Vec::new();
177312
let mut end_cur: Option<String> = None;
178-
let mut prs: Vec<GitHubPullRequests> = Vec::new();
313+
179314
loop {
180315
let var = pull_requests::Variables {
181316
owner: owner.to_string(),
182317
name: name.to_string(),
183318
first: Some(GITHUB_FETCH_SIZE),
184319
last: None,
185320
before: None,
186-
after: end_cur,
321+
after: end_cur.take(),
187322
};
188323

189324
let resp_body: GraphQlResponse<pull_requests::ResponseData> =
190325
send_query::<PullRequests>(token, var).await?.json().await?;
326+
191327
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());
328+
if let Some(repo) = data.repository {
329+
let mut batch = Vec::new();
330+
331+
if let Some(nodes) = repo.pull_requests.nodes {
332+
for pr in nodes.into_iter().flatten() {
333+
let mut assignees_list = Vec::new();
334+
if let Some(ass_nodes) = pr.assignees.nodes {
335+
for node in ass_nodes.into_iter().flatten() {
336+
assignees_list.push(node.login);
201337
}
202338
}
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());
339+
let assignees_conn = GitHubUserConnection {
340+
nodes: assignees_list,
341+
};
342+
343+
let mut rr_nodes = Vec::new();
344+
if let Some(req_conn) = pr.review_requests {
345+
if let Some(req_nodes) = req_conn.nodes {
346+
for rr in req_nodes.into_iter().flatten() {
347+
if let Some(User(user_node)) = rr.requested_reviewer {
348+
rr_nodes.push(ReviewRequestNode {
349+
requested_reviewer: Some(user_node.login),
350+
});
351+
}
211352
}
212353
}
213354
}
355+
let requests_conn = GitHubReviewRequestConnection { nodes: rr_nodes };
214356

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;
357+
let record = GitHubPullRequests {
358+
number: i32::try_from(pr.number)
359+
.context("pull request number out of i32 range")?,
360+
title: pr.title,
361+
state: pr.state.clone(),
362+
assignees: assignees_conn,
363+
review_requests: requests_conn,
364+
..Default::default()
365+
};
366+
batch.push(record);
225367
}
226-
end_cur = repository.pull_requests.page_info.end_cursor;
227-
continue;
228368
}
229-
end_cur = repository.pull_requests.page_info.end_cursor;
369+
370+
total_prs.push(batch);
371+
if !repo.pull_requests.page_info.has_next_page {
372+
break;
373+
}
374+
end_cur = repo.pull_requests.page_info.end_cursor;
230375
continue;
231376
}
232377
}
378+
233379
bail!("Failed to parse response data");
234380
}
381+
235382
Ok(total_prs)
236383
}
237384

0 commit comments

Comments
 (0)