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

Commit a14e601

Browse files
committed
Update Issues GraphQL query and related codes
- Added new fields (specified in #169) to the GraphQL query. - Restored previously commented-out project-related fields. - Explicitly mapped GraphQL custom scalar `URI` to Rust. - Made `Issue` struct support serialization/deserialization to enable storing/retrieving from the database. - Updated `GitHubIssue` struct to align with the `Issue` struct, and adjusted related functions accordingly. - Made alias for enums related with `Issue` struct. - Changed the numeric type in `parse_key`'s return value from `i64` to `i32` to align with the GraphQL API. Close: #169
1 parent 4e486b6 commit a14e601

File tree

9 files changed

+567
-59
lines changed

9 files changed

+567
-59
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 in `issues` GraphQL query, and related structs (`Issue` and
15+
`GitHubIssue`).
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
@@ -5,11 +5,11 @@ edition = "2021"
55

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

src/database.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,12 +65,8 @@ impl Database {
6565
name: &str,
6666
) -> Result<()> {
6767
for item in resp {
68-
let keystr: String = format!("{owner}/{name}#{}", item.number);
69-
Database::insert(
70-
&keystr,
71-
(&item.title, &item.author, &item.closed_at),
72-
&self.issue_tree,
73-
)?;
68+
let keystr = format!("{owner}/{name}#{}", item.number);
69+
Database::insert(&keystr, &item, &self.issue_tree)?;
7470
}
7571
Ok(())
7672
}
@@ -155,7 +151,7 @@ impl<T: TryFromKeyValue> DoubleEndedIterator for Iter<T> {
155151
}
156152
}
157153

158-
pub(crate) fn parse_key(key: &[u8]) -> Result<(String, String, i64)> {
154+
pub(crate) fn parse_key(key: &[u8]) -> Result<(String, String, i32)> {
159155
let re = Regex::new(r"(?P<owner>\S+)/(?P<name>\S+)#(?P<number>[0-9]+)").expect("valid regex");
160156
if let Some(caps) = re.captures(
161157
String::from_utf8(key.to_vec())
@@ -176,7 +172,7 @@ pub(crate) fn parse_key(key: &[u8]) -> Result<(String, String, i64)> {
176172
.name("number")
177173
.ok_or_else(|| anyhow!("invalid key"))?
178174
.as_str()
179-
.parse::<i64>()
175+
.parse::<i32>()
180176
.context("invalid key")?;
181177
Ok((owner, name, number))
182178
} else {

src/github.rs

Lines changed: 208 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,35 @@ 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

11+
use crate::github::issues::{
12+
IssuesRepositoryIssuesNodesAuthor::User as IssueAuthor,
13+
IssuesRepositoryIssuesNodesCommentsNodesAuthor::User as IssueCommentsAuthor,
14+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoInitiationOption as TodoInitOption,
15+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoPendingDays as TodoPendingDays,
16+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoPriority as TodoPriority,
17+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoSize as TodoSize,
18+
IssuesRepositoryIssuesNodesProjectItemsNodesTodoStatus as TodoStatus,
19+
IssuesRepositoryIssuesNodesSubIssuesNodesAuthor::User as SubIssueAuthor,
20+
IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNodeAuthor::User as PullRequestRefAuthor,
21+
};
22+
use crate::graphql::issue::{CommentConnection, Comment, SubIssue, ParentIssue, ProjectV2Item, ProjectV2ItemConnection, PullRequestRef};
1123
use crate::{database::Database, github::{
12-
issues::IssuesRepositoryIssuesNodesAuthor::User as userName,
1324
pull_requests::PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User,
1425
}, settings::Repository as RepoInfo};
26+
pub(crate) use issues::{IssueState, PullRequestState};
1527

1628
const GITHUB_FETCH_SIZE: i64 = 10;
1729
const GITHUB_URL: &str = "https://api.github.com/graphql";
1830
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
1931
const INIT_TIME: &str = "1992-06-05T00:00:00Z";
2032

21-
type DateTime = String;
33+
pub(crate) type DateTime = chrono::DateTime<Utc>;
34+
#[allow(clippy::upper_case_acronyms)]
35+
type URI = String;
2236

2337
#[derive(GraphQLQuery)]
2438
#[graphql(
@@ -35,17 +49,30 @@ struct Issues;
3549
)]
3650
struct PullRequests;
3751

38-
#[derive(Debug)]
52+
#[derive(Debug, Serialize, Deserialize)]
3953
pub(super) struct GitHubIssue {
40-
pub(super) number: i64,
54+
pub(super) id: String,
55+
pub(super) number: i32,
4156
pub(super) title: String,
4257
pub(super) author: String,
58+
pub(super) body: String,
59+
pub(super) state: IssueState,
60+
pub(super) assignees: Vec<String>,
61+
pub(super) labels: Vec<String>,
62+
pub(super) comments: CommentConnection,
63+
pub(super) project_items: ProjectV2ItemConnection,
64+
pub(super) sub_issues: Vec<SubIssue>,
65+
pub(super) parent: Option<ParentIssue>,
66+
pub(super) url: String,
67+
pub(super) closed_by_pull_requests: Vec<PullRequestRef>,
68+
pub(super) created_at: DateTime,
69+
pub(super) updated_at: DateTime,
4370
pub(super) closed_at: Option<DateTime>,
4471
}
4572

4673
#[derive(Debug)]
4774
pub(super) struct GitHubPullRequests {
48-
pub(super) number: i64,
75+
pub(super) number: i32,
4976
pub(super) title: String,
5077
pub(super) assignees: Vec<String>,
5178
pub(super) reviewers: Vec<String>,
@@ -116,6 +143,7 @@ pub(super) async fn fetch_periodically(
116143
}
117144
}
118145

146+
#[allow(clippy::too_many_lines)]
119147
async fn send_github_issue_query(
120148
owner: &str,
121149
name: &str,
@@ -132,38 +160,187 @@ async fn send_github_issue_query(
132160
first: Some(GITHUB_FETCH_SIZE),
133161
last: None,
134162
before: None,
135-
after: end_cur,
136-
since: Some(since.to_string()),
163+
after: end_cur.take(),
164+
since: Some(since.parse::<DateTime>()?),
137165
};
138166
let resp_body: GraphQlResponse<issues::ResponseData> =
139167
send_query::<Issues>(token, var).await?.json().await?;
140168
if let Some(data) = resp_body.data {
141169
if let Some(repository) = data.repository {
142-
if let Some(nodes) = repository.issues.nodes.as_ref() {
143-
for issue in nodes.iter().flatten() {
144-
let mut author: String = String::new();
145-
146-
let author_ref = issue.author.as_ref().context("Missing issue author")?;
147-
if let userName(on_user) = author_ref {
148-
author.clone_from(&on_user.login.clone());
149-
}
150-
issues.push(GitHubIssue {
151-
number: issue.number,
152-
title: issue.title.to_string(),
153-
author,
154-
closed_at: issue.closed_at.clone(),
155-
});
156-
}
157-
if !repository.issues.page_info.has_next_page {
158-
total_issue.push(issues);
159-
break;
160-
}
161-
end_cur = repository.issues.page_info.end_cursor;
162-
continue;
170+
let nodes = repository.issues.nodes.unwrap_or_default();
171+
for issue in nodes.into_iter().flatten() {
172+
let author = match issue.author.context("Missing issue author")? {
173+
IssueAuthor(u) => u.login,
174+
_ => String::new(),
175+
};
176+
issues.push(GitHubIssue {
177+
id: issue.id,
178+
number: issue.number.try_into().unwrap_or_default(),
179+
title: issue.title,
180+
author,
181+
body: issue.body,
182+
state: issue.state,
183+
assignees: issue
184+
.assignees
185+
.nodes
186+
.unwrap_or_default()
187+
.into_iter()
188+
.flatten()
189+
.map(|n| n.login)
190+
.collect(),
191+
labels: issue
192+
.labels
193+
.and_then(|l| l.nodes)
194+
.unwrap_or_default()
195+
.into_iter()
196+
.flatten()
197+
.map(|node| node.name)
198+
.collect(),
199+
comments: CommentConnection {
200+
total_count: issue.comments.total_count.try_into().unwrap_or_default(),
201+
nodes: issue
202+
.comments
203+
.nodes
204+
.unwrap_or_default()
205+
.into_iter()
206+
.flatten()
207+
.map(|comment| Comment {
208+
author: match comment.author {
209+
Some(IssueCommentsAuthor(u)) => u.login,
210+
_ => String::new(),
211+
},
212+
body: comment.body,
213+
created_at: comment.created_at,
214+
id: comment.id,
215+
repository_name: comment.repository.name,
216+
updated_at: comment.updated_at,
217+
url: comment.url,
218+
})
219+
.collect(),
220+
},
221+
project_items: ProjectV2ItemConnection {
222+
total_count: issue
223+
.project_items
224+
.total_count
225+
.try_into()
226+
.unwrap_or_default(),
227+
nodes: issue
228+
.project_items
229+
.nodes
230+
.unwrap_or_default()
231+
.into_iter()
232+
.flatten()
233+
.map(|node| ProjectV2Item {
234+
id: node.id,
235+
todo_status: node.todo_status.and_then(|status| match status {
236+
TodoStatus::ProjectV2ItemFieldSingleSelectValue(inner) => {
237+
inner.name
238+
}
239+
_ => None,
240+
}),
241+
todo_priority: node.todo_priority.and_then(|priority| {
242+
match priority {
243+
TodoPriority::ProjectV2ItemFieldSingleSelectValue(
244+
inner,
245+
) => inner.name,
246+
_ => None,
247+
}
248+
}),
249+
todo_size: node.todo_size.and_then(|size| match size {
250+
TodoSize::ProjectV2ItemFieldSingleSelectValue(inner) => {
251+
inner.name
252+
}
253+
_ => None,
254+
}),
255+
todo_initiation_option: node.todo_initiation_option.and_then(
256+
|init| match init {
257+
TodoInitOption::ProjectV2ItemFieldSingleSelectValue(
258+
inner,
259+
) => inner.name,
260+
_ => None,
261+
},
262+
),
263+
todo_pending_days: node.todo_pending_days.and_then(|days| {
264+
match days {
265+
TodoPendingDays::ProjectV2ItemFieldNumberValue(
266+
inner,
267+
) => inner.number,
268+
_ => None,
269+
}
270+
}),
271+
})
272+
.collect(),
273+
},
274+
sub_issues: issue
275+
.sub_issues
276+
.nodes
277+
.into_iter()
278+
.flatten()
279+
.flatten()
280+
.map(|si| SubIssue {
281+
id: si.id,
282+
number: si.number.try_into().unwrap_or_default(),
283+
title: si.title,
284+
state: si.state,
285+
closed_at: si.closed_at,
286+
created_at: si.created_at,
287+
updated_at: si.updated_at,
288+
author: match si.author {
289+
Some(SubIssueAuthor(u)) => u.login,
290+
_ => String::new(),
291+
},
292+
assignees: si
293+
.assignees
294+
.nodes
295+
.into_iter()
296+
.flatten()
297+
.flatten()
298+
.map(|n| n.login)
299+
.collect(),
300+
})
301+
.collect(),
302+
parent: issue.parent.map(|parent| ParentIssue {
303+
id: parent.id,
304+
number: parent.number.try_into().unwrap_or_default(),
305+
title: parent.title,
306+
}),
307+
url: issue.url,
308+
closed_by_pull_requests: issue
309+
.closed_by_pull_requests_references
310+
.map(|r| r.edges)
311+
.unwrap_or_default()
312+
.into_iter()
313+
.flatten()
314+
.flatten()
315+
.filter_map(|edge| {
316+
edge.node.map(|node| PullRequestRef {
317+
number: node.number.try_into().unwrap_or_default(),
318+
state: node.state,
319+
closed_at: node.closed_at,
320+
created_at: node.created_at,
321+
updated_at: node.updated_at,
322+
author: match node.author {
323+
Some(PullRequestRefAuthor(u)) => u.login,
324+
_ => String::new(),
325+
},
326+
url: node.url,
327+
})
328+
})
329+
.collect(),
330+
created_at: issue.created_at,
331+
updated_at: issue.updated_at,
332+
closed_at: issue.closed_at,
333+
});
163334
}
335+
if !repository.issues.page_info.has_next_page {
336+
total_issue.push(issues);
337+
break;
338+
}
339+
end_cur = repository.issues.page_info.end_cursor;
340+
continue;
164341
}
342+
bail!("Failed to parse response data");
165343
}
166-
bail!("Failed to parse response data");
167344
}
168345
Ok(total_issue)
169346
}
@@ -213,7 +390,7 @@ async fn send_github_pr_query(
213390
}
214391

215392
prs.push(GitHubPullRequests {
216-
number: pr.number,
393+
number: pr.number.try_into().unwrap_or_default(),
217394
title: pr.title.to_string(),
218395
assignees,
219396
reviewers,

0 commit comments

Comments
 (0)