use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::integrations::github::GithubClient;
use crate::profile::types::ContributorProfile;
use super::GithubIssueConfig;
#[derive(Debug, Deserialize)]
pub(super) struct IssueSearchResult {
#[serde(default)]
pub items: Vec<IssueItem>,
}
#[derive(Debug, Deserialize)]
pub(super) struct IssueItem {
pub number: u64,
pub html_url: String,
pub title: String,
}
#[derive(Debug, Serialize)]
pub(super) struct IssueBody {
pub title: String,
pub body: String,
pub labels: Vec<String>,
}
#[derive(Debug, Serialize)]
pub(super) struct CommentBody {
pub body: String,
}
pub(super) fn issue_title(profile: &ContributorProfile) -> String {
format!(
"[dev-profile] {} <{}>",
profile.canonical_name, profile.canonical_email
)
}
pub(super) async fn github_upsert_issue(
client: &GithubClient,
config: &GithubIssueConfig,
profile: &ContributorProfile,
markdown: &str,
) -> Result<String, crate::integrations::github::GithubError> {
use crate::integrations::github::GithubError;
let title = issue_title(profile);
let search_url = format!(
"https://api.github.com/search/issues?q=repo:{owner}/{repo}+label:{label}+in:title+{email}&type=issue",
owner = config.owner,
repo = config.repo,
label = config.label,
email = urlencoding_simple(&config.label),
);
debug!(url = %search_url, "searching for existing dev-profile issue");
let search_resp = client
.http
.get(&search_url)
.header("Authorization", format!("Bearer {}", config.token))
.header("User-Agent", &client.user_agent)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.send()
.await
.map_err(|e| GithubError::Transport(e.to_string()))?;
if !search_resp.status().is_success() {
let status = search_resp.status().as_u16();
let body = search_resp
.text()
.await
.unwrap_or_else(|_| String::from("(no body)"));
return Err(GithubError::Api { status, body });
}
let search_result: IssueSearchResult = search_resp
.json()
.await
.map_err(|e| GithubError::Transport(e.to_string()))?;
let existing = search_result.items.iter().find(|i| {
i.title.starts_with("[dev-profile]") && i.title.contains(&profile.canonical_email)
});
if let Some(issue) = existing {
let comment_url = format!(
"https://api.github.com/repos/{owner}/{repo}/issues/{number}/comments",
owner = config.owner,
repo = config.repo,
number = issue.number,
);
let comment = CommentBody {
body: markdown.to_string(),
};
let resp = client
.http
.post(&comment_url)
.header("Authorization", format!("Bearer {}", config.token))
.header("User-Agent", &client.user_agent)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.json(&comment)
.send()
.await
.map_err(|e| GithubError::Transport(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body_text = resp
.text()
.await
.unwrap_or_else(|_| String::from("(no body)"));
return Err(GithubError::Api {
status,
body: body_text,
});
}
Ok(issue.html_url.clone())
} else {
let create_url = format!(
"https://api.github.com/repos/{owner}/{repo}/issues",
owner = config.owner,
repo = config.repo,
);
let issue_body = IssueBody {
title,
body: markdown.to_string(),
labels: vec![config.label.clone()],
};
let resp = client
.http
.post(&create_url)
.header("Authorization", format!("Bearer {}", config.token))
.header("User-Agent", &client.user_agent)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.json(&issue_body)
.send()
.await
.map_err(|e| GithubError::Transport(e.to_string()))?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body_text = resp
.text()
.await
.unwrap_or_else(|_| String::from("(no body)"));
return Err(GithubError::Api {
status,
body: body_text,
});
}
#[derive(Deserialize)]
struct CreatedIssue {
html_url: String,
}
let created: CreatedIssue = resp
.json()
.await
.map_err(|e| GithubError::Transport(e.to_string()))?;
Ok(created.html_url)
}
}
pub(super) fn urlencoding_simple(s: &str) -> String {
s.replace(' ', "+").replace('@', "%40")
}