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

Commit 53ea738

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 type of the number field in the `Issue` struct from i32 to i64. Close: #169
1 parent 4e486b6 commit 53ea738

File tree

5 files changed

+523
-39
lines changed

5 files changed

+523
-39
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/github.rs

Lines changed: 200 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,39 @@
11
use std::{sync::Arc, time::Duration};
22

3-
use anyhow::{bail, Context, Result};
3+
use anyhow::{bail, 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::{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;
27+
pub(crate) use issues::PullRequestState;
1528

1629
const GITHUB_FETCH_SIZE: i64 = 10;
1730
const GITHUB_URL: &str = "https://api.github.com/graphql";
1831
const APP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
1932
const INIT_TIME: &str = "1992-06-05T00:00:00Z";
2033

2134
type DateTime = String;
35+
#[allow(clippy::upper_case_acronyms)]
36+
type URI = String;
2237

2338
#[derive(GraphQLQuery)]
2439
#[graphql(
@@ -35,12 +50,25 @@ struct Issues;
3550
)]
3651
struct PullRequests;
3752

38-
#[derive(Debug)]
53+
#[derive(Debug, Serialize, Deserialize)]
3954
pub(super) struct GitHubIssue {
55+
pub(super) id: String,
4056
pub(super) number: i64,
4157
pub(super) title: String,
4258
pub(super) author: String,
43-
pub(super) closed_at: Option<DateTime>,
59+
pub(super) body: String,
60+
pub(super) state: IssueState,
61+
pub(super) assignees: Vec<String>,
62+
pub(super) labels: Vec<String>,
63+
pub(super) comments: Vec<Comment>,
64+
pub(super) project_items: ProjectV2ItemConnection,
65+
pub(super) sub_issues: Vec<SubIssue>,
66+
pub(super) parent: Option<ParentIssue>,
67+
pub(super) url: String,
68+
pub(super) closed_by_pull_requests: Vec<PullRequestRef>,
69+
pub(super) created_at: String,
70+
pub(super) updated_at: String,
71+
pub(super) closed_at: Option<String>,
4472
}
4573

4674
#[derive(Debug)]
@@ -116,6 +144,7 @@ pub(super) async fn fetch_periodically(
116144
}
117145
}
118146

147+
#[allow(clippy::too_many_lines)]
119148
async fn send_github_issue_query(
120149
owner: &str,
121150
name: &str,
@@ -132,38 +161,182 @@ async fn send_github_issue_query(
132161
first: Some(GITHUB_FETCH_SIZE),
133162
last: None,
134163
before: None,
135-
after: end_cur,
164+
after: end_cur.take(),
136165
since: Some(since.to_string()),
137166
};
138167
let resp_body: GraphQlResponse<issues::ResponseData> =
139168
send_query::<Issues>(token, var).await?.json().await?;
140169
if let Some(data) = resp_body.data {
141170
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();
171+
let nodes = repository.issues.nodes.unwrap_or_default();
172+
for issue in nodes.into_iter().flatten() {
173+
let author = match issue.author {
174+
Some(IssueAuthor(u)) => u.login,
175+
_ => String::new(),
176+
};
145177

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;
178+
issues.push(GitHubIssue {
179+
id: issue.id,
180+
number: issue.number,
181+
title: issue.title,
182+
author,
183+
body: issue.body,
184+
state: issue.state,
185+
assignees: issue
186+
.assignees
187+
.nodes
188+
.unwrap_or_default()
189+
.into_iter()
190+
.flatten()
191+
.map(|n| n.login)
192+
.collect(),
193+
labels: issue
194+
.labels
195+
.and_then(|l| l.nodes)
196+
.unwrap_or_default()
197+
.into_iter()
198+
.flatten()
199+
.map(|node| node.name)
200+
.collect(),
201+
comments: 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+
project_items: ProjectV2ItemConnection {
221+
total_count: issue.project_items.total_count,
222+
nodes: issue
223+
.project_items
224+
.nodes
225+
.unwrap_or_default()
226+
.into_iter()
227+
.flatten()
228+
.map(|node| ProjectV2Item {
229+
id: node.id,
230+
todo_status: node.todo_status.and_then(|status| match status {
231+
TodoStatus::ProjectV2ItemFieldSingleSelectValue(inner) => {
232+
inner.name
233+
}
234+
_ => None,
235+
}),
236+
todo_priority: node.todo_priority.and_then(|priority| {
237+
match priority {
238+
TodoPriority::ProjectV2ItemFieldSingleSelectValue(
239+
inner,
240+
) => inner.name,
241+
_ => None,
242+
}
243+
}),
244+
todo_size: node.todo_size.and_then(|size| match size {
245+
TodoSize::ProjectV2ItemFieldSingleSelectValue(inner) => {
246+
inner.name
247+
}
248+
_ => None,
249+
}),
250+
todo_initiation_option: node.todo_initiation_option.and_then(
251+
|init| match init {
252+
TodoInitOption::ProjectV2ItemFieldSingleSelectValue(
253+
inner,
254+
) => inner.name,
255+
_ => None,
256+
},
257+
),
258+
todo_pending_days: node.todo_pending_days.and_then(|days| {
259+
match days {
260+
TodoPendingDays::ProjectV2ItemFieldNumberValue(
261+
inner,
262+
) => inner.number,
263+
_ => None,
264+
}
265+
}),
266+
})
267+
.collect(),
268+
},
269+
270+
sub_issues: issue
271+
.sub_issues
272+
.nodes
273+
.into_iter()
274+
.flatten()
275+
.flatten()
276+
.map(|si| SubIssue {
277+
id: si.id,
278+
number: si.number,
279+
title: si.title,
280+
state: si.state,
281+
closed_at: si.closed_at,
282+
created_at: si.created_at,
283+
updated_at: si.updated_at,
284+
author: match si.author {
285+
Some(SubIssueAuthor(u)) => u.login,
286+
_ => String::new(),
287+
},
288+
assignees: si
289+
.assignees
290+
.nodes
291+
.into_iter()
292+
.flatten()
293+
.flatten()
294+
.map(|n| n.login)
295+
.collect(),
296+
})
297+
.collect(),
298+
parent: issue.parent.map(|parent| ParentIssue {
299+
id: parent.id,
300+
number: parent.number,
301+
title: parent.title,
302+
}),
303+
url: issue.url,
304+
closed_by_pull_requests: issue
305+
.closed_by_pull_requests_references
306+
.map(|r| r.edges)
307+
.unwrap_or_default()
308+
.into_iter()
309+
.flatten()
310+
.flatten()
311+
.filter_map(|edge| {
312+
edge.node.map(|node| PullRequestRef {
313+
number: node.number,
314+
state: node.state,
315+
closed_at: node.closed_at,
316+
created_at: node.created_at,
317+
updated_at: node.updated_at,
318+
author: match node.author {
319+
Some(PullRequestRefAuthor(u)) => u.login,
320+
_ => String::new(),
321+
},
322+
url: node.url,
323+
})
324+
})
325+
.collect(),
326+
created_at: issue.created_at,
327+
updated_at: issue.updated_at,
328+
closed_at: issue.closed_at,
329+
});
163330
}
331+
if !repository.issues.page_info.has_next_page {
332+
total_issue.push(issues);
333+
break;
334+
}
335+
end_cur = repository.issues.page_info.end_cursor;
336+
continue;
164337
}
338+
bail!("Failed to parse response data");
165339
}
166-
bail!("Failed to parse response data");
167340
}
168341
Ok(total_issue)
169342
}

0 commit comments

Comments
 (0)