feat: basic pr diff rendering
This commit is contained in:
@@ -2,6 +2,7 @@ query PullRequestQuery($id: ID!) {
|
||||
node(id: $id) {
|
||||
__typename
|
||||
... on PullRequest {
|
||||
id
|
||||
title
|
||||
body
|
||||
state
|
||||
|
||||
@@ -41,6 +41,7 @@ pub(crate) struct PullRequest {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct DetailedPullRequest {
|
||||
pub(crate) id: Id,
|
||||
pub(crate) title: Arc<str>,
|
||||
pub(crate) state: PullRequestState,
|
||||
pub(crate) is_draft: bool,
|
||||
@@ -189,7 +190,7 @@ pub(crate) struct ChangedFile {
|
||||
pub(crate) change_type: ChangeType,
|
||||
pub(crate) additions: i64,
|
||||
pub(crate) deletions: i64,
|
||||
pub(crate) path: String,
|
||||
pub(crate) path: Arc<str>,
|
||||
pub(crate) viewer_viewed_state: FileViewedState,
|
||||
}
|
||||
|
||||
@@ -408,6 +409,7 @@ impl query::QueryFn for FetchPullRequest {
|
||||
})?;
|
||||
|
||||
Ok(DetailedPullRequest {
|
||||
id: Id(p.id.into()),
|
||||
title: p.title.into(),
|
||||
state: p.state,
|
||||
is_draft: p.is_draft,
|
||||
@@ -531,7 +533,7 @@ impl query::QueryFn for FetchPullRequestFileTree {
|
||||
},
|
||||
additions: node.additions,
|
||||
deletions: node.deletions,
|
||||
path: node.path,
|
||||
path: node.path.into(),
|
||||
viewer_viewed_state: node.viewer_viewed_state,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -376,6 +376,29 @@ mod tests {
|
||||
)
|
||||
.unwrap_or_else(|_| panic!("head fixture should exist for {path}"));
|
||||
}
|
||||
|
||||
let _ = fetch_pull_request_file_tree(&issues::Id::from("PR_kwDONovem85"))
|
||||
.expect("repo picker pull request file tree fixture should parse");
|
||||
|
||||
let repo_picker_file_tree_json: serde_json::Value = serde_json::from_str(
|
||||
issues_pull_request_file_tree("PR_kwDONovem85")
|
||||
.expect("repo picker pull request file tree fixture json should exist"),
|
||||
)
|
||||
.expect("repo picker pull request file tree fixture json should parse");
|
||||
|
||||
let repo_picker_file_paths = repo_picker_file_tree_json
|
||||
.get("node")
|
||||
.and_then(|node| node.get("files"))
|
||||
.and_then(|files| files.get("edges"))
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.expect("repo picker pull request file tree fixture should contain file edges")
|
||||
.iter()
|
||||
.filter_map(|edge| edge.get("node"))
|
||||
.filter_map(|node| node.get("path"))
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(repo_picker_file_paths, vec!["src/api/repo.rs"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
142
src/api/repo.rs
142
src/api/repo.rs
@@ -78,8 +78,8 @@ impl query::QueryFn for FetchFileContent {
|
||||
|
||||
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(),
|
||||
| Some(reff) => format!("repo/fetch/{}/{}/{}", self.repo_slug, self.path, reff).into(),
|
||||
| None => format!("repo/fetch/{}/{}", self.repo_slug, self.path).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,11 +94,11 @@ impl query::QueryFn for FetchFileContent {
|
||||
}
|
||||
|
||||
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),
|
||||
| Some(reff) => format!(
|
||||
"/repos/{}/contents/{}?ref={}",
|
||||
self.repo_slug, self.path, reff
|
||||
),
|
||||
| None => format!("/repos/{}/contents/{}", self.repo_slug, self.path),
|
||||
};
|
||||
|
||||
let res = c
|
||||
@@ -117,9 +117,18 @@ pub struct FetchFileDiff {
|
||||
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 = util::diff::ContentDiff;
|
||||
type Error = api::Error;
|
||||
type Data = Arc<util::diff::ContentDiff>;
|
||||
type Error = FetchFileDiffError;
|
||||
type Context = api::QueryContext;
|
||||
|
||||
fn key(&self) -> query::Key {
|
||||
@@ -134,35 +143,108 @@ impl query::QueryFn for FetchFileDiff {
|
||||
async fn fetch_content(
|
||||
r: &FileRef,
|
||||
c: &<FetchFileDiff as query::QueryFn>::Context,
|
||||
) -> Result<Option<bytes::Bytes>, 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),
|
||||
) -> 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?
|
||||
};
|
||||
|
||||
let res = c
|
||||
.github_request(Method::GET, &path)?
|
||||
.header("Accept", "application/vnd.github.raw+json")
|
||||
.send()
|
||||
.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),
|
||||
};
|
||||
|
||||
res.headers().get("Content-Type");
|
||||
let res = c
|
||||
.github_request(Method::GET, &path)?
|
||||
.header("Accept", "application/vnd.github.raw+json")
|
||||
.send()
|
||||
.await
|
||||
.map_err(api::Error::HttpError)?;
|
||||
|
||||
let bytes = api::raw_content(res).await?;
|
||||
let file::ContentType::Text = file::classify_content(&bytes) else {
|
||||
return Ok(None);
|
||||
api::raw_content(res).await?
|
||||
};
|
||||
|
||||
Ok(Some(bytes))
|
||||
match file::classify_content(&bytes) {
|
||||
| file::ContentType::Text => Ok(bytes),
|
||||
| _ => Err(FetchFileDiffError::InvalidTextContent),
|
||||
}
|
||||
}
|
||||
|
||||
let (old, new) = tokio::join!(fetch_content(&self.base, c), fetch_content(&self.head, c),);
|
||||
let (old, new) =
|
||||
tokio::try_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(),
|
||||
)),
|
||||
}
|
||||
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") })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user