diff --git a/src/fetcher/github.rs b/src/fetcher/github.rs new file mode 100644 index 0000000..97cc68b --- /dev/null +++ b/src/fetcher/github.rs @@ -0,0 +1,96 @@ +use anyhow::{Result, Context}; +use regex::Regex; +use reqwest::{Client, header::HeaderMap}; +use serde::Deserialize; +use std::env; + +#[derive(Deserialize)] +struct GitHubIssueResponse { + number: u64, + title: String, + body: Option, + html_url: String, + user: Option, + labels: Option>, + state: Option, +} + +#[derive(Deserialize)] +struct GitHubUser { + login: String, +} + +#[derive(Deserialize)] +struct GitHubLabel { + name: String, +} + +pub async fn fetch_github_issue(url: &str) -> Result { + let (owner, repo, number) = parse_github_url(url)?; + + let client = Client::new(); + let token = env::var("GITHUB_TOKEN").ok(); + + let mut headers = HeaderMap::new(); + if let Some(ref token) = token { + headers.insert("Authorization", format!("Bearer {}", token).parse()?); + } + + let api_url = env::var("GITHUB_API_URL").unwrap_or_else(|_| "https://api.github.com".to_string()); + let issue_url = format!("{}/repos/{}/{}/issues/{}", api_url, owner, repo, number); + + let response = client + .get(&issue_url) + .headers(headers) + .send() + .await + .with_context(|| "Failed to fetch GitHub issue")?; + + if !response.status().is_success() { + anyhow::bail!("Failed to fetch GitHub issue: HTTP {}", response.status()); + } + + let issue: GitHubIssueResponse = response.json().await + .with_context(|| "Failed to parse GitHub response")?; + + let labels = issue.labels.unwrap_or_default() + .into_iter() + .map(|l| l.name) + .collect(); + + let author = issue.user.map(|u| u.login).unwrap_or_default(); + + Ok(super::IssueContent { + title: issue.title, + body: issue.body.unwrap_or_default(), + number: issue.number, + url: issue.html_url, + labels, + author, + state: issue.state.unwrap_or_default(), + }) +} + +fn parse_github_url(url: &str) -> Result<(String, String, u64)> { + let re = regex::Regex::new(r"github\.com/([a-zA-Z0-9._-]+)/([a-zA-Z0-9._-]+)(?:\.git)?/(?:issues|pull)/(\d+)")?; + let caps = re.captures(url) + .ok_or_else(|| anyhow::anyhow!("Invalid GitHub URL format: {}", url))?; + + let owner = caps.get(1) + .ok_or_else(|| anyhow::anyhow!("Could not extract owner"))? + .as_str() + .to_string(); + + let repo = caps.get(2) + .ok_or_else(|| anyhow::anyhow!("Could not extract repository"))? + .as_str() + .trim_end_matches(".git") + .to_string(); + + let number = caps.get(3) + .ok_or_else(|| anyhow::anyhow!("Could not extract issue number"))? + .as_str() + .parse::()?; + + Ok((owner, repo, number)) +}