Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/db/issue_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ where
) -> Result<IssueData<'db, T>> {
let repo = issue.repository().to_string();
let issue_number = issue.number as i32;
Self::load_raw(db, repo, issue_number, key).await
}

pub async fn load_raw(
db: &'db mut DbClient,
repo: String,
issue_number: i32,
key: &str,
) -> Result<IssueData<'db, T>> {
let transaction = db.transaction().await?;
transaction
.execute("LOCK TABLE issue_data", &[])
Expand Down
1 change: 1 addition & 0 deletions src/github.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ pub use webhook::event::*;
pub use webhook::webhook;

pub type UserId = u64;
pub type IssueNumber = u64;
pub type PullRequestNumber = u64;
24 changes: 23 additions & 1 deletion src/github/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use reqwest::Body;
use reqwest::header::{AUTHORIZATION, USER_AGENT};
use reqwest::{Client, Request, RequestBuilder, Response, StatusCode};
use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Deserializer};
use std::time::{Duration, SystemTime};
use tracing as log;

Expand Down Expand Up @@ -380,12 +381,33 @@ pub struct GraphQlErrors {
pub struct GraphQlError {
#[serde(default)]
pub message: String,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_path")]
pub path: Vec<String>,
#[serde(default, rename = "type")]
pub type_: String,
}

fn deserialize_path<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize the field into a vector of generic JSON Values first
let raw_vector: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;

// Map each JSON Value cleanly into a String
let string_vector = raw_vector
.into_iter()
.map(|value| match value {
serde_json::Value::String(s) => s,
serde_json::Value::Number(n) => n.to_string(),
serde_json::Value::Bool(b) => b.to_string(),
_ => value.to_string(),
})
.collect();

Ok(string_vector)
}

impl std::fmt::Display for GraphQlErrors {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for (i, err) in self.errors.iter().enumerate() {
Expand Down
95 changes: 95 additions & 0 deletions src/github/queries/closing_issues_references.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use anyhow::Context as _;
use chrono::{DateTime, Utc};

use crate::github::{GithubClient, IssueNumber, PullRequestNumber};

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PullRequestWithClosingIssuesReferences {
pub number: PullRequestNumber,
pub updated_at: DateTime<Utc>,
pub closing_issues_references: Vec<ClosingIssueReference>,
}

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClosingIssueReference {
pub number: IssueNumber,
}

impl GithubClient {
pub async fn closing_issues_references(
&self,
owner: &str,
repo: &str,
) -> anyhow::Result<Vec<PullRequestWithClosingIssuesReferences>> {
fn page_info(data: &serde_json::Value) -> (bool, Option<String>) {
let has_next = data["pageInfo"]["hasNextPage"].as_bool().unwrap_or(false);
let end_cursor = data["pageInfo"]["endCursor"]
.as_str()
.map(|s| s.to_string());
(has_next, end_cursor)
}

let mut prs_cursor: Option<String> = None;
let mut prs = Vec::<PullRequestWithClosingIssuesReferences>::new();

loop {
let mut data = self
.graphql_query(
r##"
query ($owner: String!, $repo: String!, $after: String) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 100, after: $after, states: OPEN) {
pageInfo {
hasNextPage
endCursor
}
nodes {
number
updatedAt
closingIssuesReferences(first: 10) {
nodes {
number
}
}
}
}
}
}
"##,
serde_json::json!({
"owner": owner,
"repo": repo,
"after": prs_cursor.as_deref(),
}),
)
.await
.context("failed to fetch opened issues")?;

let mut value = data["data"]["repository"]["pullRequests"].take();

for val in value["nodes"].as_array_mut().context("no issues nodes")? {
prs.push(PullRequestWithClosingIssuesReferences {
number: val["number"].as_u64().context("no issue number")?,
updated_at: serde_json::from_value(val["updatedAt"].take())?,
closing_issues_references: serde_json::from_value(
val["closingIssuesReferences"]["nodes"].take(),
)?,
});
}

let (has_next, new_end_cursor) = page_info(&value);

if new_end_cursor.is_some() {
prs_cursor = new_end_cursor;
}

if !has_next {
break;
}
}

Ok(prs)
}
}
107 changes: 107 additions & 0 deletions src/github/queries/issues_assigned.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use anyhow::Context as _;
use chrono::{DateTime, Utc};

use crate::github::{GithubClient, IssueNumber};

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitHubIssueAssigned {
pub number: IssueNumber,
pub updated_at: DateTime<Utc>,
pub assignees: Vec<GitHubAssignee>,
}

#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GitHubAssignee {
pub login: String,
#[serde(alias = "databaseId")]
pub id: u64,
}

impl GithubClient {
pub async fn issues_assigned(
&self,
owner: &str,
repo: &str,
) -> anyhow::Result<Vec<GitHubIssueAssigned>> {
fn page_info(data: &serde_json::Value) -> (bool, Option<String>) {
let has_next = data["pageInfo"]["hasNextPage"].as_bool().unwrap_or(false);
let end_cursor = data["pageInfo"]["endCursor"]
.as_str()
.map(|s| s.to_string());
(has_next, end_cursor)
}

let mut issues_cursor: Option<String> = None;
let mut issues = Vec::<GitHubIssueAssigned>::new();

loop {
let mut data = self
.graphql_query(
r##"
query(
$owner: String!,
$repo: String!,
$after: String
) {
repository(owner: $owner, name: $repo) {
issues(
first: 100,
after: $after,
filterBy: {
states: [OPEN],
assignee: "*"
}
) {
pageInfo {
hasNextPage
endCursor
}
nodes {
number
updatedAt
assignees(first: 5) {
nodes {
login
databaseId
}
}
}
}
}
}
"##,
serde_json::json!({
"owner": owner,
"repo": repo,
"after": issues_cursor.as_deref(),
}),
)
.await
.context("failed to fetch opened issues")?;

let mut value = data["data"]["repository"]["issues"].take();

for val in value["nodes"].as_array_mut().context("no issues nodes")? {
issues.push(GitHubIssueAssigned {
number: val["number"].as_u64().context("no issue number")?,
updated_at: serde_json::from_value(val["updatedAt"].take())?,
assignees: serde_json::from_value(val["assignees"]["nodes"].take())?,
});
}

let (has_next, new_end_cursor) = page_info(&value);

if new_end_cursor.is_some() {
issues_cursor = new_end_cursor;
}

if !has_next {
break;
}
}

Ok(issues)
}
}
2 changes: 2 additions & 0 deletions src/github/queries/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pub(crate) mod closing_issues_references;
pub(crate) mod issue_with_comments;
pub(crate) mod issues_assigned;
pub(crate) mod user_comments_in_org;
pub(crate) mod user_contributions;
pub(crate) mod user_info;
Expand Down
2 changes: 1 addition & 1 deletion src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use std::fmt;
use std::sync::Arc;
use tracing as log;

mod assign;
pub(crate) mod assign;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jobs are not handlers, and even though I know that there are some jobs inside the handlers module currently, I don't find that very readable. What do you think about having a separate module (directory) for jobs?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about having a separate module (directory) for jobs?

Conceptually I like the idea, but when the handler and the job is so tightly integrated into each other I think it's better to have them close to each other.

mod autolabel;
mod backport;
mod bot_pull_requests;
Expand Down
6 changes: 5 additions & 1 deletion src/handlers/assign.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ use tokio_postgres::Client as DbClient;
use tracing as log;

mod messages;
pub(crate) mod release_inactive_assignments;

#[cfg(test)]
mod tests {
Expand All @@ -59,6 +60,9 @@ mod tests {
// Special account that we use to prevent assignment.
const GHOST_ACCOUNT: &str = "ghost";

/// Key for the state in the database
const ASSIGN_KEY: &str = "ASSIGN";

/// Key for the state in the database
const PREVIOUS_REVIEWERS_KEY: &str = "previous-reviewers";

Expand Down Expand Up @@ -893,7 +897,7 @@ pub(super) async fn handle_command(
} else {
let mut client = ctx.db.get().await;
let mut e: EditIssueBody<'_, AssignData> =
EditIssueBody::load(&mut client, issue, "ASSIGN").await?;
EditIssueBody::load(&mut client, issue, ASSIGN_KEY).await?;
let d = e.data_mut();

let to_assign = match cmd {
Expand Down
Loading