use std::path::{Path, PathBuf};
use crate::error::{Result, TrvError};
use crate::model::{DiffFile, DiffHunk, DiffLine, FileStatus, LineOrigin};
use super::traits::{VcsBackend, VcsInfo, VcsType};
pub struct FileBackend {
info: VcsInfo,
file_path: PathBuf,
}
impl FileBackend {
pub fn new(path: &str) -> Result<Self> {
let file_path = std::fs::canonicalize(path).map_err(|e| {
TrvError::Io(std::io::Error::new(
e.kind(),
format!("Cannot open file '{path}': {e}"),
))
})?;
if !file_path.is_file() {
return Err(TrvError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("'{path}' is not a file"),
)));
}
let root_path = file_path.clone();
let info = VcsInfo {
root_path,
head_commit: "file".to_string(),
branch_name: None,
vcs_type: VcsType::File,
};
Ok(Self { info, file_path })
}
}
impl VcsBackend for FileBackend {
fn info(&self) -> &VcsInfo {
&self.info
}
fn get_working_tree_diff(&self) -> Result<Vec<DiffFile>> {
let content = std::fs::read_to_string(&self.file_path)?;
let lines: Vec<&str> = content.lines().collect();
let rel_path = self
.file_path
.file_name()
.map_or_else(|| self.file_path.clone(), PathBuf::from);
if lines.is_empty() {
let file = DiffFile {
old_path: None,
new_path: Some(rel_path),
status: FileStatus::Added,
hunks: vec![],
is_binary: false,
is_too_large: false,
is_commit_message: false,
};
return Ok(vec![file]);
}
let line_contents: Vec<String> = lines.iter().map(|l| l.replace('\t', " ")).collect();
let mut diff_lines = Vec::with_capacity(lines.len());
for (i, content) in line_contents.iter().enumerate() {
let line_num = (i + 1) as u32;
diff_lines.push(DiffLine {
origin: LineOrigin::Addition,
content: content.clone(),
old_lineno: None,
new_lineno: Some(line_num),
highlighted_spans: None,
});
}
let total_lines = lines.len() as u32;
let hunk = DiffHunk {
header: format!("@@ -0,0 +1,{total_lines} @@"),
lines: diff_lines,
old_start: 0,
old_count: 0,
new_start: 1,
new_count: total_lines,
};
let file = DiffFile {
old_path: None,
new_path: Some(rel_path),
status: FileStatus::Added,
hunks: vec![hunk],
is_binary: false,
is_too_large: false,
is_commit_message: false,
};
Ok(vec![file])
}
fn fetch_context_lines(
&self,
_file_path: &Path,
_file_status: FileStatus,
start_line: u32,
end_line: u32,
) -> Result<Vec<DiffLine>> {
if start_line > end_line || start_line == 0 {
return Ok(Vec::new());
}
let content = std::fs::read_to_string(&self.file_path)?;
let lines: Vec<&str> = content.lines().collect();
let mut result = Vec::new();
for line_num in start_line..=end_line {
let idx = (line_num - 1) as usize;
if idx < lines.len() {
result.push(DiffLine {
origin: LineOrigin::Context,
content: lines[idx].to_string(),
old_lineno: Some(line_num),
new_lineno: Some(line_num),
highlighted_spans: None,
});
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_file_returns_ok_with_zero_hunks() {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let file_path = dir.path().join("empty.rs");
std::fs::write(&file_path, "").expect("failed to write empty file");
let backend =
FileBackend::new(file_path.to_str().unwrap()).expect("failed to create backend");
let result = backend.get_working_tree_diff();
assert!(result.is_ok(), "empty file should not be an error");
let files = result.unwrap();
assert_eq!(files.len(), 1);
assert!(
files[0].hunks.is_empty(),
"empty file should have zero hunks"
);
assert_eq!(files[0].status, FileStatus::Added);
}
#[test]
fn files_in_same_directory_have_different_session_keys() {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let file_a = dir.path().join("a.rs");
let file_b = dir.path().join("b.rs");
std::fs::write(&file_a, "a").expect("write a");
std::fs::write(&file_b, "b").expect("write b");
let backend_a =
FileBackend::new(file_a.to_str().unwrap()).expect("failed to create backend a");
let backend_b =
FileBackend::new(file_b.to_str().unwrap()).expect("failed to create backend b");
assert_ne!(
backend_a.info().root_path,
backend_b.info().root_path,
"files in the same directory must have different root_path for unique session keys"
);
}
#[test]
fn non_empty_file_returns_hunks() {
let dir = tempfile::tempdir().expect("failed to create temp dir");
let file_path = dir.path().join("hello.rs");
std::fs::write(&file_path, "fn main() {}\n").expect("failed to write file");
let backend =
FileBackend::new(file_path.to_str().unwrap()).expect("failed to create backend");
let result = backend.get_working_tree_diff();
assert!(result.is_ok());
let files = result.unwrap();
assert_eq!(files.len(), 1);
assert_eq!(files[0].hunks.len(), 1);
assert!(!files[0].hunks[0].lines.is_empty());
}
}