2026-05-23 18:45:44 +01:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
use reqwest::Method;
|
2026-05-06 01:42:38 +08:00
|
|
|
use serde::Deserialize;
|
|
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
use crate::{
|
2026-05-23 18:45:44 +01:00
|
|
|
api, query,
|
2026-05-23 12:28:45 +01:00
|
|
|
util::{self, file},
|
2026-05-18 22:30:46 +08:00
|
|
|
};
|
2026-05-06 01:42:38 +08:00
|
|
|
|
|
|
|
|
#[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,
|
|
|
|
|
}
|
2026-04-20 15:13:26 +01:00
|
|
|
|
2026-05-18 22:30:46 +08:00
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct FileRef {
|
2026-05-23 18:45:44 +01:00
|
|
|
pub repo_slug: Arc<str>,
|
|
|
|
|
pub path: Arc<str>,
|
|
|
|
|
pub reff: Option<Arc<str>>,
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-20 15:13:26 +01:00
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct List;
|
|
|
|
|
|
2026-04-21 20:30:41 +01:00
|
|
|
impl query::QueryFn for List {
|
2026-05-06 01:42:38 +08:00
|
|
|
type Data = Vec<Repository>;
|
|
|
|
|
type Error = api::Error;
|
|
|
|
|
type Context = api::QueryContext;
|
2026-04-20 15:13:26 +01:00
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
fn key(&self) -> query::Key {
|
|
|
|
|
"repo/list".into()
|
2026-04-20 15:13:26 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-06 01:42:38 +08:00
|
|
|
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(¶ms)
|
|
|
|
|
.send()
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
api::parse_response(res).await
|
2026-04-20 15:13:26 +01:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-18 22:30:46 +08:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct FetchFileContent {
|
2026-05-23 18:45:44 +01:00
|
|
|
pub repo_slug: Arc<str>,
|
|
|
|
|
pub path: Arc<str>,
|
|
|
|
|
pub reff: Option<Arc<str>>,
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-05-24 16:44:10 +01:00
|
|
|
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
|
|
|
|
|
| None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(),
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
2026-05-24 16:44:10 +01:00
|
|
|
| Some(reff) => format!(
|
|
|
|
|
"/repos/{}/contents/{}?ref={}",
|
|
|
|
|
self.repo_slug, self.path, reff
|
|
|
|
|
),
|
|
|
|
|
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
|
2026-05-18 22:30:46 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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)]
|
2026-05-23 12:28:45 +01:00
|
|
|
pub struct FetchFileDiff {
|
2026-05-18 22:30:46 +08:00
|
|
|
pub base: FileRef,
|
|
|
|
|
pub head: FileRef,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 16:44:10 +01:00
|
|
|
#[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,
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 12:28:45 +01:00
|
|
|
impl query::QueryFn for FetchFileDiff {
|
2026-05-24 16:44:10 +01:00
|
|
|
type Data = Arc<util::diff::ContentDiff>;
|
|
|
|
|
type Error = FetchFileDiffError;
|
2026-05-18 22:30:46 +08:00
|
|
|
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,
|
2026-05-23 12:28:45 +01:00
|
|
|
c: &<FetchFileDiff as query::QueryFn>::Context,
|
2026-05-24 16:44:10 +01:00
|
|
|
) -> 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?
|
2026-05-18 22:30:46 +08:00
|
|
|
};
|
|
|
|
|
|
2026-05-24 16:44:10 +01:00
|
|
|
#[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?
|
2026-05-18 22:30:46 +08:00
|
|
|
};
|
|
|
|
|
|
2026-05-24 16:44:10 +01:00
|
|
|
match file::classify_content(&bytes) {
|
|
|
|
|
| file::ContentType::Text => Ok(bytes),
|
|
|
|
|
| _ => Err(FetchFileDiffError::InvalidTextContent),
|
|
|
|
|
}
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-24 16:44:10 +01:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-18 22:30:46 +08:00
|
|
|
|
2026-05-24 16:44:10 +01:00
|
|
|
#[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()),
|
|
|
|
|
},
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
2026-05-24 16:44:10 +01:00
|
|
|
.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") })
|
|
|
|
|
);
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
|
|
|
|
}
|