Files
novem/src/api/repo.rs

251 lines
7.1 KiB
Rust

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<String>,
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<str>,
pub path: Arc<str>,
pub reff: Option<Arc<str>>,
}
#[derive(Clone)]
pub struct List;
impl query::QueryFn for List {
type Data = Vec<Repository>;
type Error = api::Error;
type Context = api::QueryContext;
fn key(&self) -> query::Key {
"repo/list".into()
}
async fn run(&self, c: &Self::Context) -> Result<Self::Data, Self::Error> {
#[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(&params)
.send()
.await?;
api::parse_response(res).await
}
}
#[derive(Clone)]
pub struct FetchFileContent {
pub repo_slug: Arc<str>,
pub path: Arc<str>,
pub reff: Option<Arc<str>>,
}
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<Self::Data, Self::Error> {
#[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<util::diff::ContentDiff>;
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<Self::Data, Self::Error> {
async fn fetch_content(
r: &FileRef,
c: &<FetchFileDiff as query::QueryFn>::Context,
) -> Result<bytes::Bytes, FetchFileDiffError> {
#[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") })
);
}
}