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

Commit 6d5f9ea

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. Close: #169
1 parent 4e486b6 commit 6d5f9ea

File tree

6 files changed

+483
-49
lines changed

6 files changed

+483
-49
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 `Issue` GraphQL query, and related structs (`Issue` and
15+
`GitHubIssue`).
1416

1517
### Changed
1618

src/database.rs

Lines changed: 2 additions & 6 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
}

src/github.rs

Lines changed: 155 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
use std::{sync::Arc, time::Duration};
2+
use std::str::FromStr;
23

3-
use anyhow::{bail, Context, Result};
4+
use anyhow::{bail, Error, Result};
45
use chrono::Utc;
56
use graphql_client::{GraphQLQuery, QueryBody, Response as GraphQlResponse};
67
use reqwest::{Client, RequestBuilder, Response};
7-
use serde::Serialize;
8+
use serde::{Deserialize, Serialize};
89
use tokio::time;
910
use tracing::error;
10-
11+
use crate::graphql::issue::{Comment, SubIssue, ParentIssue, ProjectV2Item, ProjectV2ItemConnection, PullRequestRef};
1112
use crate::{database::Database, github::{
12-
issues::IssuesRepositoryIssuesNodesAuthor::User as userName,
1313
pull_requests::PullRequestsRepositoryPullRequestsNodesReviewRequestsNodesRequestedReviewer::User,
1414
}, settings::Repository as RepoInfo};
15-
1615
const GITHUB_FETCH_SIZE: i64 = 10;
1716
const GITHUB_URL: &str = "https://api.github.com/graphql";
1817
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
1918
const INIT_TIME: &str = "1992-06-05T00:00:00Z";
2019

2120
type DateTime = String;
21+
#[allow(clippy::upper_case_acronyms)]
22+
type URI = String;
2223

2324
#[derive(GraphQLQuery)]
2425
#[graphql(
@@ -27,6 +28,18 @@ type DateTime = String;
2728
response_derives = "Debug"
2829
)]
2930
struct Issues;
31+
pub(crate) use issues::IssueState;
32+
33+
impl FromStr for IssueState {
34+
type Err = Error;
35+
fn from_str(s: &str) -> Result<Self, Self::Err> {
36+
match s {
37+
"OPEN" => Ok(IssueState::OPEN),
38+
"CLOSED" => Ok(IssueState::CLOSED),
39+
other => Err(Error::msg(format!("Unknown IssueState: {other}"))),
40+
}
41+
}
42+
}
3043

3144
#[derive(GraphQLQuery)]
3245
#[graphql(
@@ -35,12 +48,25 @@ struct Issues;
3548
)]
3649
struct PullRequests;
3750

38-
#[derive(Debug)]
51+
#[derive(Debug, Serialize, Deserialize)]
3952
pub(super) struct GitHubIssue {
53+
pub(super) id: String,
4054
pub(super) number: i64,
4155
pub(super) title: String,
4256
pub(super) author: String,
43-
pub(super) closed_at: Option<DateTime>,
57+
pub(super) body: String,
58+
pub(super) state: IssueState,
59+
pub(super) assignees: Vec<String>,
60+
pub(super) labels: Vec<String>,
61+
pub(super) comments: Vec<Comment>,
62+
pub(super) project_items: ProjectV2ItemConnection,
63+
pub(super) sub_issues: Vec<SubIssue>,
64+
pub(super) parent: Option<ParentIssue>,
65+
pub(super) url: String,
66+
pub(super) closed_by_pull_requests: Vec<PullRequestRef>,
67+
pub(super) created_at: String,
68+
pub(super) updated_at: String,
69+
pub(super) closed_at: Option<String>,
4470
}
4571

4672
#[derive(Debug)]
@@ -116,6 +142,7 @@ pub(super) async fn fetch_periodically(
116142
}
117143
}
118144

145+
#[allow(clippy::too_many_lines)]
119146
async fn send_github_issue_query(
120147
owner: &str,
121148
name: &str,
@@ -132,38 +159,137 @@ async fn send_github_issue_query(
132159
first: Some(GITHUB_FETCH_SIZE),
133160
last: None,
134161
before: None,
135-
after: end_cur,
162+
after: end_cur.take(),
136163
since: Some(since.to_string()),
137164
};
138165
let resp_body: GraphQlResponse<issues::ResponseData> =
139166
send_query::<Issues>(token, var).await?.json().await?;
140167
if let Some(data) = resp_body.data {
141168
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();
169+
let nodes = repository.issues.nodes.unwrap_or_default();
170+
for issue in nodes.into_iter().flatten() {
171+
let author = match issue.author {
172+
Some(issues::IssuesRepositoryIssuesNodesAuthor::User(u)) => u.login,
173+
_ => String::new(),
174+
};
145175

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;
176+
issues.push(GitHubIssue {
177+
id: issue.id,
178+
number: issue.number,
179+
title: issue.title,
180+
author,
181+
body: issue.body,
182+
state: issue.state,
183+
assignees: issue.assignees.nodes.unwrap_or_default().into_iter().flatten().map(|n| n.login).collect(),
184+
labels: issue.labels.and_then(|l| l.nodes).unwrap_or_default().into_iter().flatten().map(|node| node.name).collect(),
185+
comments: issue.comments.nodes.unwrap_or_default().into_iter().flatten().map(|comment| Comment {
186+
author: match comment.author {
187+
Some(issues::IssuesRepositoryIssuesNodesCommentsNodesAuthor::User(u)) => u.login,
188+
_ => String::new(),
189+
},
190+
body: comment.body,
191+
created_at: comment.created_at,
192+
id: comment.id,
193+
repository_name: comment.repository.name,
194+
updated_at: comment.updated_at,
195+
url: comment.url,
196+
}).collect(),
197+
project_items: ProjectV2ItemConnection {
198+
total_count: issue.project_items.total_count.try_into().unwrap_or_default(),
199+
nodes: issue
200+
.project_items
201+
.nodes
202+
.unwrap_or_default()
203+
.into_iter()
204+
.flatten()
205+
.map(|node| ProjectV2Item {
206+
id: node.id,
207+
todo_status: node.todo_status.map(|e| format!("{e:?}")),
208+
todo_priority: node.todo_priority.map(|e| format!("{e:?}")),
209+
todo_size: node.todo_size.map(|e| format!("{e:?}")),
210+
todo_initiation_option:
211+
node.todo_initiation_option.map(|e| format!("{e:?}")),
212+
todo_pending_days: node.todo_pending_days.map(|e| {
213+
format!("{e:?}")
214+
.chars()
215+
.filter(char::is_ascii_digit)
216+
.collect::<String>()
217+
.parse::<i32>()
218+
.unwrap_or_default()
219+
}),
220+
221+
})
222+
.collect(),
223+
},
224+
225+
sub_issues: issue
226+
.sub_issues
227+
.nodes
228+
.into_iter()
229+
.flatten()
230+
.flatten()
231+
.map(|si| SubIssue {
232+
id: si.id,
233+
number: si.number.try_into().unwrap_or_default(),
234+
title: si.title,
235+
state: si.state,
236+
closed_at: si.closed_at,
237+
created_at: si.created_at,
238+
updated_at: si.updated_at,
239+
author: match si.author {
240+
Some(issues::IssuesRepositoryIssuesNodesSubIssuesNodesAuthor::User(u)) => u.login,
241+
_ => String::new(),
242+
},
243+
assignees: si
244+
.assignees
245+
.nodes
246+
.into_iter()
247+
.flatten()
248+
.flatten()
249+
.map(|n| n.login)
250+
.collect(),
251+
})
252+
.collect(),
253+
parent: issue.parent.map(|parent| ParentIssue {
254+
id: parent.id,
255+
number: i32::try_from(parent.number).unwrap_or_default(),
256+
title: parent.title,
257+
}),
258+
url: issue.url,
259+
closed_by_pull_requests: issue
260+
.closed_by_pull_requests_references
261+
.map(|r| r.edges)
262+
.unwrap_or_default()
263+
.into_iter()
264+
.flatten()
265+
.flatten()
266+
.filter_map(|edge| edge.node.map(|node| PullRequestRef {
267+
number: i32::try_from(node.number).unwrap_or_default(),
268+
state: format!("{:?}", node.state),
269+
closed_at: node.closed_at,
270+
created_at: node.created_at,
271+
updated_at: node.updated_at,
272+
author: match node.author {
273+
Some(issues::IssuesRepositoryIssuesNodesClosedByPullRequestsReferencesEdgesNodeAuthor::User(u)) => u.login,
274+
_ => String::new(),
275+
},
276+
url: node.url,
277+
}))
278+
.collect(),
279+
created_at: issue.created_at,
280+
updated_at: issue.updated_at,
281+
closed_at: issue.closed_at,
282+
});
283+
}
284+
if !repository.issues.page_info.has_next_page {
285+
total_issue.push(issues);
286+
break;
163287
}
288+
end_cur = repository.issues.page_info.end_cursor;
289+
continue;
164290
}
291+
bail!("Failed to parse response data");
165292
}
166-
bail!("Failed to parse response data");
167293
}
168294
Ok(total_issue)
169295
}

0 commit comments

Comments
 (0)