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-23 18:45:44 +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-23 18:45:44 +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-23 12:28:45 +01:00
|
|
|
impl query::QueryFn for FetchFileDiff {
|
|
|
|
|
type Data = util::diff::ContentDiff;
|
2026-05-18 22:30:46 +08:00
|
|
|
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<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-18 22:30:46 +08:00
|
|
|
) -> Result<Option<bytes::Bytes>, api::Error> {
|
|
|
|
|
let path = match &r.reff {
|
2026-05-23 18:45:44 +01:00
|
|
|
| Some(reff) => format!("/repos/{}/contents/{}?ref={}", r.repo_slug, r.path, reff),
|
|
|
|
|
| None => format!("/repos/{}/contents/{}", r.repo_slug, r.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?;
|
|
|
|
|
|
|
|
|
|
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) {
|
2026-05-23 18:45:44 +01:00
|
|
|
| (Ok(Some(old)), Ok(Some(new))) => Ok(util::diff::diff_content(old, new)),
|
|
|
|
|
| _ => Err(api::Error::MalformedResponse(
|
|
|
|
|
"failed to fetch content".to_string(),
|
|
|
|
|
)),
|
2026-05-18 22:30:46 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|