#![allow(dead_code, unused_variables, missing_docs)]
use crate::file_system::{Directory, DirectoryContents, Path};
use std::{cell::RefCell, cmp::Ordering, ops::Deref, rc::Rc};
use thiserror::Error;
#[cfg(feature = "serialize")]
use serde::{ser, Serialize, Serializer};
pub mod git;
#[derive(Debug, Error)]
#[error("A diff error occurred: {reason}")]
pub struct DiffError {
reason: String,
}
impl From<String> for DiffError {
fn from(reason: String) -> Self {
DiffError { reason }
}
}
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Diff {
pub created: Vec<CreateFile>,
pub deleted: Vec<DeleteFile>,
pub moved: Vec<MoveFile>,
pub copied: Vec<CopyFile>,
pub modified: Vec<ModifiedFile>,
}
impl Default for Diff {
fn default() -> Self {
Self::new()
}
}
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CreateFile(pub Path);
#[cfg_attr(feature = "serialize", derive(Serialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DeleteFile(pub Path);
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MoveFile {
pub old_path: Path,
pub new_path: Path,
}
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CopyFile {
pub old_path: Path,
pub new_path: Path,
}
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ModifiedFile {
pub path: Path,
pub diff: FileDiff,
}
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(tag = "type", rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum FileDiff {
Binary,
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Plain {
hunks: Vec<Hunk>,
},
}
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hunk {
pub header: Line,
pub lines: Vec<LineDiff>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Line(pub(crate) Vec<u8>);
impl From<Vec<u8>> for Line {
fn from(v: Vec<u8>) -> Self {
Self(v)
}
}
impl From<String> for Line {
fn from(s: String) -> Self {
Self(s.into_bytes())
}
}
#[cfg(feature = "serialize")]
impl Serialize for Line {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = std::str::from_utf8(&self.0).map_err(ser::Error::custom)?;
serializer.serialize_str(s)
}
}
#[cfg_attr(
feature = "serialize",
derive(Serialize),
serde(tag = "type", rename_all = "camelCase")
)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LineDiff {
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Addition { line: Line, line_num: u32 },
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Deletion { line: Line, line_num: u32 },
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
Context {
line: Line,
line_num_old: u32,
line_num_new: u32,
},
}
impl LineDiff {
pub fn addition(line: impl Into<Line>, line_num: u32) -> Self {
Self::Addition {
line: line.into(),
line_num,
}
}
pub fn deletion(line: impl Into<Line>, line_num: u32) -> Self {
Self::Deletion {
line: line.into(),
line_num,
}
}
pub fn context(line: impl Into<Line>, line_num_old: u32, line_num_new: u32) -> Self {
Self::Context {
line: line.into(),
line_num_old,
line_num_new,
}
}
}
impl Diff {
pub fn new() -> Self {
Diff {
created: Vec::new(),
deleted: Vec::new(),
moved: Vec::new(),
copied: Vec::new(),
modified: Vec::new(),
}
}
pub fn diff(left: Directory, right: Directory) -> Result<Diff, DiffError> {
let mut diff = Diff::new();
let path = Rc::new(RefCell::new(Path::from_labels(right.current(), &[])));
Diff::collect_diff(&left, &right, &path, &mut diff)?;
Ok(diff)
}
fn collect_diff(
old: &Directory,
new: &Directory,
parent_path: &Rc<RefCell<Path>>,
diff: &mut Diff,
) -> Result<(), String> {
let mut old_iter = old.iter();
let mut new_iter = new.iter();
let mut old_entry_opt = old_iter.next();
let mut new_entry_opt = new_iter.next();
while old_entry_opt.is_some() || new_entry_opt.is_some() {
match (&old_entry_opt, &new_entry_opt) {
(Some(ref old_entry), Some(ref new_entry)) => {
match new_entry.label().cmp(&old_entry.label()) {
Ordering::Greater => {
diff.add_deleted_files(old_entry, parent_path)?;
old_entry_opt = old_iter.next();
},
Ordering::Less => {
diff.add_created_files(new_entry, parent_path)?;
new_entry_opt = new_iter.next();
},
Ordering::Equal => match (new_entry, old_entry) {
(
DirectoryContents::File {
name: new_file_name,
file: new_file,
},
DirectoryContents::File {
name: old_file_name,
file: old_file,
},
) => {
if old_file.size != new_file.size
|| old_file.checksum() != new_file.checksum()
{
let mut path = parent_path.borrow().clone();
path.push(new_file_name.clone());
diff.add_modified_file(path, vec![]);
}
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
(
DirectoryContents::File {
name: new_file_name,
file: new_file,
},
DirectoryContents::Directory(old_dir),
) => {
let mut path = parent_path.borrow().clone();
path.push(new_file_name.clone());
diff.add_created_file(path);
diff.add_deleted_files(old_entry, parent_path)?;
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
(
DirectoryContents::Directory(new_dir),
DirectoryContents::File {
name: old_file_name,
file: old_file,
},
) => {
let mut path = parent_path.borrow().clone();
path.push(old_file_name.clone());
diff.add_created_files(new_entry, parent_path)?;
diff.add_deleted_file(path);
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
(
DirectoryContents::Directory(new_dir),
DirectoryContents::Directory(old_dir),
) => {
parent_path.borrow_mut().push(new_dir.current().clone());
Diff::collect_diff(
old_dir.deref(),
new_dir.deref(),
parent_path,
diff,
)?;
parent_path.borrow_mut().pop();
old_entry_opt = old_iter.next();
new_entry_opt = new_iter.next();
},
},
}
},
(Some(ref old_entry), None) => {
diff.add_deleted_files(old_entry, parent_path)?;
old_entry_opt = old_iter.next();
},
(None, Some(ref new_entry)) => {
diff.add_created_files(new_entry, parent_path)?;
new_entry_opt = new_iter.next();
},
(None, None) => break,
}
}
Ok(())
}
fn collect_files_from_entry<F, T>(
entry: &DirectoryContents,
parent_path: &Rc<RefCell<Path>>,
mapper: F,
) -> Result<Vec<T>, String>
where
F: Fn(Path) -> T + Copy,
{
match entry {
DirectoryContents::Directory(dir) => Diff::collect_files(dir, parent_path, mapper),
DirectoryContents::File { name, .. } => {
let mut path = parent_path.borrow().clone();
path.push(name.clone());
Ok(vec![mapper(path)])
},
}
}
fn collect_files<F, T>(
dir: &Directory,
parent_path: &Rc<RefCell<Path>>,
mapper: F,
) -> Result<Vec<T>, String>
where
F: Fn(Path) -> T + Copy,
{
let mut files: Vec<T> = Vec::new();
Diff::collect_files_inner(dir, parent_path, mapper, &mut files)?;
Ok(files)
}
fn collect_files_inner<'a, F, T>(
dir: &'a Directory,
parent_path: &Rc<RefCell<Path>>,
mapper: F,
files: &mut Vec<T>,
) -> Result<(), String>
where
F: Fn(Path) -> T + Copy,
{
parent_path.borrow_mut().push(dir.current());
for entry in dir.iter() {
match entry {
DirectoryContents::Directory(subdir) => {
Diff::collect_files_inner(&subdir, parent_path, mapper, files)?;
},
DirectoryContents::File { name, .. } => {
let mut path = parent_path.borrow().clone();
path.push(name);
files.push(mapper(path));
},
}
}
parent_path.borrow_mut().pop();
Ok(())
}
pub(crate) fn add_modified_file(&mut self, path: Path, hunks: Vec<Hunk>) {
self.modified.push(ModifiedFile {
path,
diff: FileDiff::Plain { hunks },
});
}
pub(crate) fn add_moved_file(&mut self, old_path: Path, new_path: Path) {
self.moved.push(MoveFile { old_path, new_path });
}
pub(crate) fn add_copied_file(&mut self, old_path: Path, new_path: Path) {
self.copied.push(CopyFile { old_path, new_path });
}
pub(crate) fn add_modified_binary_file(&mut self, path: Path) {
self.modified.push(ModifiedFile {
path,
diff: FileDiff::Binary,
});
}
pub(crate) fn add_created_file(&mut self, path: Path) {
self.created.push(CreateFile(path));
}
fn add_created_files(
&mut self,
dc: &DirectoryContents,
parent_path: &Rc<RefCell<Path>>,
) -> Result<(), String> {
let mut new_files: Vec<CreateFile> =
Diff::collect_files_from_entry(dc, &parent_path, CreateFile)?;
self.created.append(&mut new_files);
Ok(())
}
pub(crate) fn add_deleted_file(&mut self, path: Path) {
self.deleted.push(DeleteFile(path));
}
fn add_deleted_files(
&mut self,
dc: &DirectoryContents,
parent_path: &Rc<RefCell<Path>>,
) -> Result<(), String> {
let mut new_files: Vec<DeleteFile> =
Diff::collect_files_from_entry(dc, &parent_path, DeleteFile)?;
self.deleted.append(&mut new_files);
Ok(())
}
}
#[cfg(test)]
mod tests {
use crate::{
diff::*,
file_system::{unsound, *},
};
use pretty_assertions::assert_eq;
#[test]
fn test_create_file() {
let directory = Directory::root();
let mut new_directory = Directory::root();
new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
let diff = Diff::diff(directory, new_directory).expect("diff failed");
let expected_diff = Diff {
created: vec![CreateFile(Path::with_root(&[unsound::label::new(
"banana.rs",
)]))],
deleted: vec![],
copied: vec![],
moved: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_delete_file() {
let mut directory = Directory::root();
directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
let new_directory = Directory::root();
let diff = Diff::diff(directory, new_directory).expect("diff failed");
let expected_diff = Diff {
created: vec![],
deleted: vec![DeleteFile(Path::with_root(&[unsound::label::new(
"banana.rs",
)]))],
moved: vec![],
copied: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_modify_file() {
let mut directory = Directory::root();
directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana"));
let mut new_directory = Directory::root();
new_directory.insert_file(unsound::path::new("banana.rs"), File::new(b"use banana;"));
let diff = Diff::diff(directory, new_directory).expect("diff failed");
let expected_diff = Diff {
created: vec![],
deleted: vec![],
moved: vec![],
copied: vec![],
modified: vec![ModifiedFile {
path: Path::with_root(&[unsound::label::new("banana.rs")]),
diff: FileDiff::Plain { hunks: vec![] },
}],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_create_directory() {
let directory = Directory::root();
let mut new_directory = Directory::root();
new_directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana"),
);
let diff = Diff::diff(directory, new_directory).expect("diff failed");
let expected_diff = Diff {
created: vec![CreateFile(Path::with_root(&[
unsound::label::new("src"),
unsound::label::new("banana.rs"),
]))],
deleted: vec![],
moved: vec![],
copied: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_delete_directory() {
let mut directory = Directory::root();
directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana"),
);
let new_directory = Directory::root();
let diff = Diff::diff(directory, new_directory).expect("diff failed");
let expected_diff = Diff {
created: vec![],
deleted: vec![DeleteFile(Path::with_root(&[
unsound::label::new("src"),
unsound::label::new("banana.rs"),
]))],
moved: vec![],
copied: vec![],
modified: vec![],
};
assert_eq!(diff, expected_diff)
}
#[test]
fn test_modify_file_directory() {
let mut directory = Directory::root();
directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana"),
);
let mut new_directory = Directory::root();
new_directory.insert_file(
unsound::path::new("src/banana.rs"),
File::new(b"use banana;"),
);
let diff = Diff::diff(directory, new_directory).expect("diff failed");
let expected_diff = Diff {
created: vec![],
deleted: vec![],
moved: vec![],
copied: vec![],
modified: vec![ModifiedFile {
path: Path::with_root(&[
unsound::label::new("src"),
unsound::label::new("banana.rs"),
]),
diff: FileDiff::Plain { hunks: vec![] },
}],
};
assert_eq!(diff, expected_diff)
}
}