use std::sync::Arc; use reqwest::Method; use serde::Deserialize; use crate::{ api, query, util::{self, file}, }; #[derive(Debug, Deserialize)] pub struct Repository { pub id: u64, pub name: String, pub full_name: String, pub private: bool, pub html_url: String, pub description: Option, pub default_branch: String, pub owner: Owner, } #[derive(Debug, Deserialize)] pub struct Owner { pub login: String, pub id: api::user::Id, pub avatar_url: String, pub html_url: String, } #[derive(Debug, Clone)] pub struct FileRef { pub repo_slug: Arc, pub path: Arc, pub reff: Option>, } #[derive(Clone)] pub struct List; impl query::QueryFn for List { type Data = Vec; type Error = api::Error; type Context = api::QueryContext; fn key(&self) -> query::Key { "repo/list".into() } async fn run(&self, c: &Self::Context) -> Result { #[cfg(debug_assertions)] if c.should_use_fixtures { return super::mock::list_repos(); } let params = [("sort", "updated"), ("per_page", "100")]; let res = c .github_request(reqwest::Method::GET, "/user/repos")? .query(¶ms) .send() .await?; api::parse_response(res).await } } #[derive(Clone)] pub struct FetchFileContent { pub repo_slug: Arc, pub path: Arc, pub reff: Option>, } impl query::QueryFn for FetchFileContent { type Data = bytes::Bytes; type Error = api::Error; type Context = api::QueryContext; fn key(&self) -> query::Key { match &self.reff { | Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(), | None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(), } } async fn run(&self, c: &Self::Context) -> Result { #[cfg(debug_assertions)] if c.should_use_fixtures { return super::mock::fetch_file_content( &self.repo_slug, &self.path, self.reff.as_deref(), ); } let path = match &self.reff { | Some(reff) => format!( "/repos/{}/contents/{}?ref={}", self.repo_slug, self.path, reff ), | None => format!("/repos/{}/contents/{}", self.repo_slug, self.path), }; let res = c .github_request(Method::GET, &path)? .header("Accept", "application/vnd.github.raw+json") .send() .await?; api::raw_content(res).await } } #[derive(Debug, Clone)] pub struct FetchFileDiff { pub base: FileRef, pub head: FileRef, } #[derive(Debug, thiserror::Error)] pub enum FetchFileDiffError { #[error("api error when fetching file diff: {0:?}")] ApiError(#[from] api::Error), #[error("invalid utf8 content or unsupported file type")] InvalidTextContent, } impl query::QueryFn for FetchFileDiff { type Data = Arc; type Error = FetchFileDiffError; type Context = api::QueryContext; fn key(&self) -> query::Key { format!( "repo/diff/{}/{}/{}", self.base.repo_slug, self.base.path, self.head.path ) .into() } async fn run(&self, c: &Self::Context) -> Result { async fn fetch_content( r: &FileRef, c: &::Context, ) -> Result { #[cfg(debug_assertions)] let bytes = if c.should_use_fixtures { super::mock::fetch_file_content(&r.repo_slug, &r.path, r.reff.as_deref())? } else { let path = match &r.reff { | Some(reff) => { format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff) } | None => format!("/repos/{}/contents/{}", r.repo_slug, r.path), }; let res = c .github_request(Method::GET, &path)? .header("Accept", "application/vnd.github.raw+json") .send() .await .map_err(api::Error::HttpError)?; api::raw_content(res).await? }; #[cfg(not(debug_assertions))] let bytes = { let path = match &r.reff { | Some(reff) => { format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff) } | None => format!("/repos/{}/contents/{}", r.repo_slug, r.path), }; let res = c .github_request(Method::GET, &path)? .header("Accept", "application/vnd.github.raw+json") .send() .await .map_err(api::Error::HttpError)?; api::raw_content(res).await? }; match file::classify_content(&bytes) { | file::ContentType::Text => Ok(bytes), | _ => Err(FetchFileDiffError::InvalidTextContent), } } let (old, new) = tokio::try_join!(fetch_content(&self.base, c), fetch_content(&self.head, c),)?; util::diff::diff_content(old, new) .map(|diff| Arc::new(diff)) .ok_or(FetchFileDiffError::InvalidTextContent) } } #[cfg(test)] mod tests { use crate::query::QueryFn; use super::*; #[tokio::test] async fn fetch_file_diff_uses_repo_file_content_fixtures() { let pull_request = super::super::mock::fetch_pull_request(&api::issues::Id::from("PR_kwDONovem84")) .expect("pull request fixture should parse"); let diff = FetchFileDiff { base: FileRef { repo_slug: pull_request.base_repo_slug.clone(), path: Arc::from("src/query.rs"), reff: Some(pull_request.base_ref.clone()), }, head: FileRef { repo_slug: pull_request.head_repo_slug.clone(), path: Arc::from("src/query.rs"), reff: Some(pull_request.head_ref.clone()), }, } .run(&api::QueryContext { http: reqwest::Client::new(), auth: None, github: api::GithubCredentials { base_url: "", client_id: "", }, should_use_fixtures: true, }) .await .expect("fetch file diff should succeed from fixtures"); assert!(diff.len() > 0); assert!( (0..diff.len()) .filter_map(|i| diff.get(i).old_content.as_deref()) .any(|line| { line.contains("pub struct CachedSelection") }) ); assert!( (0..diff.len()) .filter_map(|i| diff.get(i).new_content.as_deref()) .any(|line| { line.contains("pub struct CachedQueryState") }) ); } }