use futures::{FutureExt, TryFutureExt}; use reqwest::Method; use serde::Deserialize; use tokio::sync::OwnedRwLockReadGuard; use crate::{ api, query::{self, Query, fetch_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: String, pub path: String, 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: String, pub path: String, 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, } impl query::QueryFn for FetchFileDiff { type Data = util::diff::ContentDiff; type Error = api::Error; 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, api::Error> { 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?; res.headers().get("Content-Type"); let bytes = api::raw_content(res).await?; let file::ContentType::Text = file::classify_content(&bytes) else { return Ok(None); }; Ok(Some(bytes)) } let (old, new) = tokio::join!(fetch_content(&self.base, c), fetch_content(&self.head, c),); match (old, new) { | (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)), | _ => Err(api::Error::MalformedResponse( "failed to fetch content".to_string(), )), } } }