use std::path::{Path, PathBuf};
use crate::error::Result;
use crate::forge::traits::{ForgeBackend, ForgeFileLinesRequest, ForgeRepository, PrSessionKey};
use crate::model::{DiffLine, FileStatus};
use crate::vcs::VcsBackend;
pub trait ContextProvider {
fn fetch_context_lines(
&self,
old_path: Option<&PathBuf>,
new_path: Option<&PathBuf>,
file_status: FileStatus,
start_line: u32,
end_line: u32,
) -> Result<Vec<DiffLine>>;
}
pub struct VcsContextProvider<'a> {
pub vcs: &'a dyn VcsBackend,
}
impl ContextProvider for VcsContextProvider<'_> {
fn fetch_context_lines(
&self,
old_path: Option<&PathBuf>,
new_path: Option<&PathBuf>,
file_status: FileStatus,
start_line: u32,
end_line: u32,
) -> Result<Vec<DiffLine>> {
let path: &Path = match new_path.or(old_path) {
Some(p) => p.as_path(),
None => return Ok(Vec::new()),
};
self.vcs
.fetch_context_lines(path, file_status, start_line, end_line)
}
}
pub struct ForgeContextProvider<'a> {
pub forge: &'a dyn ForgeBackend,
pub repository: ForgeRepository,
pub base_sha: String,
pub head_sha: String,
}
impl<'a> ForgeContextProvider<'a> {
pub fn for_pr(
forge: &'a dyn ForgeBackend,
key: &PrSessionKey,
base_sha: impl Into<String>,
) -> Self {
Self {
forge,
repository: key.repository.clone(),
base_sha: base_sha.into(),
head_sha: key.head_sha.clone(),
}
}
}
impl ContextProvider for ForgeContextProvider<'_> {
fn fetch_context_lines(
&self,
old_path: Option<&PathBuf>,
new_path: Option<&PathBuf>,
file_status: FileStatus,
start_line: u32,
end_line: u32,
) -> Result<Vec<DiffLine>> {
let side = ForgeFileLinesRequest::side_for_status(file_status);
let Some(path) = ForgeFileLinesRequest::path_for_side(side, old_path, new_path) else {
return Ok(Vec::new());
};
let request = ForgeFileLinesRequest {
repository: self.repository.clone(),
base_sha: self.base_sha.clone(),
head_sha: self.head_sha.clone(),
path,
status: file_status,
side,
start_line,
end_line,
};
self.forge.fetch_file_lines(request)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forge::traits::{
ForgeFileSide, PagedPullRequests, PullRequestDetails, PullRequestListQuery,
PullRequestTarget,
};
use crate::model::LineOrigin;
use std::cell::RefCell;
struct CapturingForge {
seen: RefCell<Vec<ForgeFileLinesRequest>>,
response: Vec<DiffLine>,
}
impl ForgeBackend for CapturingForge {
fn list_pull_requests(&self, _query: PullRequestListQuery) -> Result<PagedPullRequests> {
unimplemented!()
}
fn get_pull_request(&self, _target: PullRequestTarget) -> Result<PullRequestDetails> {
unimplemented!()
}
fn get_pull_request_diff(&self, _pr: &PullRequestDetails) -> Result<String> {
unimplemented!()
}
fn fetch_file_lines(&self, request: ForgeFileLinesRequest) -> Result<Vec<DiffLine>> {
self.seen.borrow_mut().push(request);
Ok(self.response.clone())
}
fn list_review_threads(
&self,
_pr: &PullRequestDetails,
) -> Result<Vec<crate::forge::remote_comments::RemoteReviewThread>> {
Ok(Vec::new())
}
fn list_pull_request_commits(
&self,
_pr: &PullRequestDetails,
) -> Result<Vec<crate::forge::traits::PullRequestCommit>> {
Ok(Vec::new())
}
fn get_pull_request_commit_range_diff(
&self,
_pr: &PullRequestDetails,
_start_sha: &str,
_end_sha: &str,
) -> Result<String> {
unimplemented!()
}
fn create_review(
&self,
_pr: &PullRequestDetails,
_request: crate::forge::traits::CreateReviewRequest<'_>,
) -> Result<crate::forge::traits::GhCreateReviewResponse> {
unimplemented!()
}
}
fn repo() -> ForgeRepository {
ForgeRepository::github("github.com", "agavra", "tuicr")
}
fn key() -> PrSessionKey {
PrSessionKey::new(repo(), 125, "headsha".to_string())
}
fn make_line(text: &str) -> DiffLine {
DiffLine {
origin: LineOrigin::Context,
content: text.to_string(),
old_lineno: Some(1),
new_lineno: Some(1),
highlighted_spans: None,
}
}
#[test]
fn should_use_head_side_for_modified_file() {
let forge = CapturingForge {
seen: RefCell::new(Vec::new()),
response: vec![make_line("a")],
};
let provider = ForgeContextProvider::for_pr(&forge, &key(), "basesha");
let new_path = PathBuf::from("src/lib.rs");
let _ = provider
.fetch_context_lines(None, Some(&new_path), FileStatus::Modified, 1, 1)
.unwrap();
let seen = forge.seen.borrow();
assert_eq!(seen.len(), 1);
assert_eq!(seen[0].side, ForgeFileSide::Head);
assert_eq!(seen[0].sha(), "headsha");
assert_eq!(seen[0].path, PathBuf::from("src/lib.rs"));
}
#[test]
fn should_use_base_side_for_deleted_file() {
let forge = CapturingForge {
seen: RefCell::new(Vec::new()),
response: vec![],
};
let provider = ForgeContextProvider::for_pr(&forge, &key(), "basesha");
let old_path = PathBuf::from("src/gone.rs");
let _ = provider
.fetch_context_lines(Some(&old_path), None, FileStatus::Deleted, 1, 1)
.unwrap();
let seen = forge.seen.borrow();
assert_eq!(seen.len(), 1);
assert_eq!(seen[0].side, ForgeFileSide::Base);
assert_eq!(seen[0].sha(), "basesha");
assert_eq!(seen[0].path, PathBuf::from("src/gone.rs"));
}
#[test]
fn should_pick_old_path_for_rename_base_side() {
let old = PathBuf::from("old.rs");
let new = PathBuf::from("new.rs");
assert_eq!(
ForgeFileLinesRequest::path_for_side(ForgeFileSide::Base, Some(&old), Some(&new),),
Some(old.clone()),
);
assert_eq!(
ForgeFileLinesRequest::path_for_side(ForgeFileSide::Head, Some(&old), Some(&new),),
Some(new),
);
}
#[test]
fn should_short_circuit_when_path_missing() {
let forge = CapturingForge {
seen: RefCell::new(Vec::new()),
response: vec![make_line("x")],
};
let provider = ForgeContextProvider::for_pr(&forge, &key(), "basesha");
let result = provider
.fetch_context_lines(None, None, FileStatus::Modified, 1, 5)
.unwrap();
assert!(result.is_empty());
assert!(forge.seen.borrow().is_empty());
}
}