use crate::{svn_result, with_tmp_pool, Error};
#[derive(Debug, Clone, Copy, Default)]
pub struct DiffOptions {
pub ignore_whitespace: bool,
pub ignore_eol_style: bool,
pub show_c_function: bool,
}
impl DiffOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
self.ignore_whitespace = ignore;
self
}
pub fn with_ignore_eol_style(mut self, ignore: bool) -> Self {
self.ignore_eol_style = ignore;
self
}
pub fn with_show_c_function(mut self, show: bool) -> Self {
self.show_c_function = show;
self
}
}
pub struct DiffHunk {
ptr: *mut subversion_sys::svn_diff_hunk_t,
}
impl DiffHunk {
#[allow(dead_code)]
unsafe fn from_raw(ptr: *mut subversion_sys::svn_diff_hunk_t) -> Self {
Self { ptr }
}
pub fn original_start(&self) -> u64 {
unsafe { subversion_sys::svn_diff_hunk_get_original_start(self.ptr).into() }
}
pub fn original_length(&self) -> u64 {
unsafe { subversion_sys::svn_diff_hunk_get_original_length(self.ptr).into() }
}
pub fn modified_start(&self) -> u64 {
unsafe { subversion_sys::svn_diff_hunk_get_modified_start(self.ptr).into() }
}
pub fn modified_length(&self) -> u64 {
unsafe { subversion_sys::svn_diff_hunk_get_modified_length(self.ptr).into() }
}
pub fn leading_context(&self) -> u64 {
unsafe { subversion_sys::svn_diff_hunk_get_leading_context(self.ptr).into() }
}
pub fn trailing_context(&self) -> u64 {
unsafe { subversion_sys::svn_diff_hunk_get_trailing_context(self.ptr).into() }
}
}
pub struct Diff {
ptr: *mut subversion_sys::svn_diff_t,
_pool: apr::Pool<'static>,
}
impl Diff {
unsafe fn from_raw(ptr: *mut subversion_sys::svn_diff_t, pool: apr::Pool<'static>) -> Self {
Self { ptr, _pool: pool }
}
pub fn contains_changes(&self) -> bool {
unsafe { subversion_sys::svn_diff_contains_diffs(self.ptr) != 0 }
}
pub fn contains_conflicts(&self) -> bool {
unsafe { subversion_sys::svn_diff_contains_conflicts(self.ptr) != 0 }
}
pub fn as_ptr(&self) -> *mut subversion_sys::svn_diff_t {
self.ptr
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct FileOptions {
pub ignore_space: IgnoreSpace,
pub ignore_eol_style: bool,
pub show_c_function: bool,
}
impl FileOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
self.ignore_space = if ignore {
IgnoreSpace::Change
} else {
IgnoreSpace::None
};
self
}
pub fn with_ignore_eol_style(mut self, ignore: bool) -> Self {
self.ignore_eol_style = ignore;
self
}
pub fn with_show_c_function(mut self, show: bool) -> Self {
self.show_c_function = show;
self
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum IgnoreSpace {
#[default]
None,
Change,
All,
}
impl From<IgnoreSpace> for subversion_sys::svn_diff_file_ignore_space_t {
fn from(ignore: IgnoreSpace) -> Self {
match ignore {
IgnoreSpace::None => {
subversion_sys::svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_none
}
IgnoreSpace::Change => {
subversion_sys::svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_change
}
IgnoreSpace::All => {
subversion_sys::svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_all
}
}
}
}
pub fn file_diff(
original: &std::path::Path,
modified: &std::path::Path,
options: FileOptions,
) -> Result<Diff, Error<'static>> {
let original_cstr = std::ffi::CString::new(original.to_string_lossy().as_ref())?;
let modified_cstr = std::ffi::CString::new(modified.to_string_lossy().as_ref())?;
let pool = apr::Pool::new();
let mut diff_ptr = std::ptr::null_mut();
let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
unsafe {
(*diff_options).ignore_space = options.ignore_space.into();
(*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
(*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
}
let err = unsafe {
subversion_sys::svn_diff_file_diff_2(
&mut diff_ptr,
original_cstr.as_ptr(),
modified_cstr.as_ptr(),
diff_options,
pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}
pub fn file_diff3(
original: &std::path::Path,
modified: &std::path::Path,
latest: &std::path::Path,
options: FileOptions,
) -> Result<Diff, Error<'static>> {
let original_cstr = std::ffi::CString::new(original.to_string_lossy().as_ref())?;
let modified_cstr = std::ffi::CString::new(modified.to_string_lossy().as_ref())?;
let latest_cstr = std::ffi::CString::new(latest.to_string_lossy().as_ref())?;
let pool = apr::Pool::new();
let mut diff_ptr = std::ptr::null_mut();
let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
unsafe {
(*diff_options).ignore_space = options.ignore_space.into();
(*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
(*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
}
let err = unsafe {
subversion_sys::svn_diff_file_diff3_2(
&mut diff_ptr,
original_cstr.as_ptr(),
modified_cstr.as_ptr(),
latest_cstr.as_ptr(),
diff_options,
pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}
pub fn file_diff4(
original: &std::path::Path,
modified: &std::path::Path,
latest: &std::path::Path,
ancestor: &std::path::Path,
options: FileOptions,
) -> Result<Diff, Error<'static>> {
let original_cstr = std::ffi::CString::new(original.to_string_lossy().as_ref())?;
let modified_cstr = std::ffi::CString::new(modified.to_string_lossy().as_ref())?;
let latest_cstr = std::ffi::CString::new(latest.to_string_lossy().as_ref())?;
let ancestor_cstr = std::ffi::CString::new(ancestor.to_string_lossy().as_ref())?;
let pool = apr::Pool::new();
let mut diff_ptr = std::ptr::null_mut();
let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
unsafe {
(*diff_options).ignore_space = options.ignore_space.into();
(*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
(*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
}
let err = unsafe {
subversion_sys::svn_diff_file_diff4_2(
&mut diff_ptr,
original_cstr.as_ptr(),
modified_cstr.as_ptr(),
latest_cstr.as_ptr(),
ancestor_cstr.as_ptr(),
diff_options,
pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}
pub fn file_output_unified(
output_stream: &mut crate::io::Stream,
diff: &Diff,
original_path: &std::path::Path,
modified_path: &std::path::Path,
original_header: Option<&str>,
modified_header: Option<&str>,
header_encoding: &str,
context_size: i32,
) -> Result<(), Error<'static>> {
let original_path_cstr = std::ffi::CString::new(original_path.to_string_lossy().as_ref())?;
let modified_path_cstr = std::ffi::CString::new(modified_path.to_string_lossy().as_ref())?;
let header_encoding_cstr = std::ffi::CString::new(header_encoding)?;
let original_header_cstr = original_header.map(std::ffi::CString::new).transpose()?;
let modified_header_cstr = modified_header.map(std::ffi::CString::new).transpose()?;
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_diff_file_output_unified4(
output_stream.as_mut_ptr(),
diff.as_ptr(),
original_path_cstr.as_ptr(),
modified_path_cstr.as_ptr(),
original_header_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
modified_header_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
header_encoding_cstr.as_ptr(),
std::ptr::null(), 1, context_size,
None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn mem_string_diff(
original: &str,
modified: &str,
options: FileOptions,
) -> Result<Diff, Error<'static>> {
let pool = apr::Pool::new();
let mut diff_ptr = std::ptr::null_mut();
let original_svn_str = subversion_sys::svn_string_t {
data: original.as_ptr() as *const std::os::raw::c_char,
len: original.len(),
};
let modified_svn_str = subversion_sys::svn_string_t {
data: modified.as_ptr() as *const std::os::raw::c_char,
len: modified.len(),
};
let diff_options = unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
unsafe {
(*diff_options).ignore_space = options.ignore_space.into();
(*diff_options).ignore_eol_style = if options.ignore_eol_style { 1 } else { 0 };
(*diff_options).show_c_function = if options.show_c_function { 1 } else { 0 };
}
let err = unsafe {
subversion_sys::svn_diff_mem_string_diff(
&mut diff_ptr,
&original_svn_str,
&modified_svn_str,
diff_options,
pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(unsafe { Diff::from_raw(diff_ptr, pool) })
}
#[derive(Debug, Clone, Copy)]
pub enum ConflictDisplayStyle {
ModifiedLatest,
ResolvedModifiedLatest,
ModifiedOriginalLatest,
OnlyConflicts,
}
impl From<ConflictDisplayStyle> for subversion_sys::svn_diff_conflict_display_style_t {
fn from(style: ConflictDisplayStyle) -> Self {
match style {
ConflictDisplayStyle::ModifiedLatest => {
subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_latest
}
ConflictDisplayStyle::ResolvedModifiedLatest => {
subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_resolved_modified_latest
}
ConflictDisplayStyle::ModifiedOriginalLatest => {
subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_original_latest
}
ConflictDisplayStyle::OnlyConflicts => {
subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_only_conflicts
}
}
}
}
pub fn file_output_merge(
output_stream: &mut crate::io::Stream,
diff: &Diff,
original_path: &std::path::Path,
modified_path: &std::path::Path,
latest_path: &std::path::Path,
conflict_original: Option<&str>,
conflict_modified: Option<&str>,
conflict_latest: Option<&str>,
conflict_separator: Option<&str>,
conflict_style: ConflictDisplayStyle,
) -> Result<(), Error<'static>> {
let original_path_cstr = std::ffi::CString::new(original_path.to_string_lossy().as_ref())?;
let modified_path_cstr = std::ffi::CString::new(modified_path.to_string_lossy().as_ref())?;
let latest_path_cstr = std::ffi::CString::new(latest_path.to_string_lossy().as_ref())?;
let conflict_original_cstr = conflict_original.map(std::ffi::CString::new).transpose()?;
let conflict_modified_cstr = conflict_modified.map(std::ffi::CString::new).transpose()?;
let conflict_latest_cstr = conflict_latest.map(std::ffi::CString::new).transpose()?;
let conflict_separator_cstr = conflict_separator.map(std::ffi::CString::new).transpose()?;
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_diff_file_output_merge3(
output_stream.as_mut_ptr(),
diff.as_ptr(),
original_path_cstr.as_ptr(),
modified_path_cstr.as_ptr(),
latest_path_cstr.as_ptr(),
conflict_original_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
conflict_modified_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
conflict_latest_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
conflict_separator_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
conflict_style.into(),
None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub unsafe fn output(
diff: &Diff,
output_baton: *mut std::ffi::c_void,
output_fns: &subversion_sys::svn_diff_output_fns_t,
) -> Result<(), Error<'static>> {
let err = unsafe {
subversion_sys::svn_diff_output2(
diff.as_ptr(),
output_baton,
output_fns,
None, std::ptr::null_mut(), )
};
svn_result(err)
}
pub fn file_output_unified_with_options(
output_stream: &mut crate::io::Stream,
diff: &Diff,
original_path: &std::path::Path,
modified_path: &std::path::Path,
original_header: Option<&str>,
modified_header: Option<&str>,
header_encoding: &str,
relative_to_dir: Option<&std::path::Path>,
show_c_function: bool,
context_size: i32,
) -> Result<(), Error<'static>> {
let original_path_cstr = std::ffi::CString::new(original_path.to_string_lossy().as_ref())?;
let modified_path_cstr = std::ffi::CString::new(modified_path.to_string_lossy().as_ref())?;
let header_encoding_cstr = std::ffi::CString::new(header_encoding)?;
let original_header_cstr = original_header.map(std::ffi::CString::new).transpose()?;
let modified_header_cstr = modified_header.map(std::ffi::CString::new).transpose()?;
let relative_to_dir_cstr = relative_to_dir
.map(|p| std::ffi::CString::new(p.to_string_lossy().as_ref()))
.transpose()?;
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_diff_file_output_unified4(
output_stream.as_mut_ptr(),
diff.as_ptr(),
original_path_cstr.as_ptr(),
modified_path_cstr.as_ptr(),
original_header_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
modified_header_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
header_encoding_cstr.as_ptr(),
relative_to_dir_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
if show_c_function { 1 } else { 0 },
context_size,
None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::tempdir;
#[test]
fn test_file_options() {
let options = FileOptions::default()
.with_ignore_whitespace(true)
.with_ignore_eol_style(true)
.with_show_c_function(false);
assert_eq!(options.ignore_space, IgnoreSpace::Change);
assert!(options.ignore_eol_style);
assert!(!options.show_c_function);
}
#[test]
fn test_mem_string_diff() {
let original = "line 1\nline 2\nline 3\n";
let modified = "line 1\nline 2 modified\nline 3\n";
let options = FileOptions::default();
let diff = mem_string_diff(original, modified, options).unwrap();
assert!(diff.contains_changes());
}
#[test]
fn test_file_diff() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("original.txt");
let modified_path = temp_dir.path().join("modified.txt");
let mut original_file = std::fs::File::create(&original_path)?;
original_file.write_all(b"line 1\nline 2\nline 3\n")?;
let mut modified_file = std::fs::File::create(&modified_path)?;
modified_file.write_all(b"line 1\nline 2 modified\nline 3\n")?;
let options = FileOptions::default();
let diff = file_diff(&original_path, &modified_path, options)?;
assert!(diff.contains_changes());
assert!(!diff.contains_conflicts());
Ok(())
}
#[test]
fn test_diff_identical_files() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("original.txt");
let modified_path = temp_dir.path().join("modified.txt");
let content = b"line 1\nline 2\nline 3\n";
std::fs::write(&original_path, content)?;
std::fs::write(&modified_path, content)?;
let options = FileOptions::default();
let diff = file_diff(&original_path, &modified_path, options)?;
assert!(!diff.contains_changes());
Ok(())
}
#[test]
fn test_file_output_unified() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("original.txt");
let modified_path = temp_dir.path().join("modified.txt");
std::fs::write(&original_path, b"line 1\nline 2\nline 3\n")?;
std::fs::write(&modified_path, b"line 1\nline 2 modified\nline 3\nline 4\n")?;
let options = FileOptions::default();
let diff = file_diff(&original_path, &modified_path, options)?;
let mut stringbuf = crate::io::StringBuf::new();
let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);
file_output_unified(
&mut stream,
&diff,
&original_path,
&modified_path,
Some("Original File"),
Some("Modified File"),
"UTF-8",
3,
)?;
let output = stringbuf.to_string();
assert!(output.contains("---"));
assert!(output.contains("+++"));
assert!(output.contains("@@"));
Ok(())
}
#[test]
fn test_file_output_merge() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("original.txt");
let modified_path = temp_dir.path().join("modified.txt");
let latest_path = temp_dir.path().join("latest.txt");
std::fs::write(&original_path, b"line 1\nline 2\nline 3\n")?;
std::fs::write(&modified_path, b"line 1\nline 2 modified\nline 3\n")?;
std::fs::write(&latest_path, b"line 1\nline 2 latest\nline 3\n")?;
let options = FileOptions::default();
let diff = file_diff3(&modified_path, &original_path, &latest_path, options)?;
let mut stringbuf = crate::io::StringBuf::new();
let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);
file_output_merge(
&mut stream,
&diff,
&original_path,
&modified_path,
&latest_path,
None,
None,
None,
None,
ConflictDisplayStyle::ModifiedLatest,
)?;
let output = stringbuf.to_string();
assert!(!output.is_empty());
Ok(())
}
#[test]
fn test_file_output_merge_with_diff3_style() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("original.txt");
let modified_path = temp_dir.path().join("modified.txt");
let latest_path = temp_dir.path().join("latest.txt");
std::fs::write(&original_path, b"common line\noriginal line\ncommon end\n")?;
std::fs::write(&modified_path, b"common line\nmodified line\ncommon end\n")?;
std::fs::write(&latest_path, b"common line\nlatest line\ncommon end\n")?;
let options = FileOptions::default();
let diff = file_diff3(&modified_path, &original_path, &latest_path, options)?;
let mut stringbuf = crate::io::StringBuf::new();
let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);
file_output_merge(
&mut stream,
&diff,
&original_path,
&modified_path,
&latest_path,
Some("Modified"),
Some("Original"),
Some("Latest"),
Some("======="),
ConflictDisplayStyle::ModifiedOriginalLatest,
)?;
let output = stringbuf.to_string();
assert!(!output.is_empty());
Ok(())
}
#[test]
fn test_file_output_unified_with_options() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("src/original.c");
let modified_path = temp_dir.path().join("src/modified.c");
std::fs::create_dir_all(original_path.parent().unwrap())?;
let original_content = b"void foo() {\n int x = 1;\n}\n\nvoid bar() {\n return;\n}\n";
let modified_content = b"void foo() {\n int x = 2;\n}\n\nvoid bar() {\n return;\n}\n";
std::fs::write(&original_path, original_content)?;
std::fs::write(&modified_path, modified_content)?;
let options = FileOptions::default();
let diff = file_diff(&original_path, &modified_path, options)?;
let mut stringbuf = crate::io::StringBuf::new();
let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);
file_output_unified_with_options(
&mut stream,
&diff,
&original_path,
&modified_path,
Some("Original Version"),
Some("Modified Version"),
"UTF-8",
Some(&temp_dir.path()),
true, 5, )?;
let output = stringbuf.to_string();
assert!(output.contains("---"));
assert!(output.contains("+++"));
Ok(())
}
#[test]
fn test_conflict_display_style() {
let style: subversion_sys::svn_diff_conflict_display_style_t =
ConflictDisplayStyle::ModifiedLatest.into();
assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_latest);
let style: subversion_sys::svn_diff_conflict_display_style_t =
ConflictDisplayStyle::ModifiedOriginalLatest.into();
assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_modified_original_latest);
let style: subversion_sys::svn_diff_conflict_display_style_t =
ConflictDisplayStyle::ResolvedModifiedLatest.into();
assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_resolved_modified_latest);
let style: subversion_sys::svn_diff_conflict_display_style_t =
ConflictDisplayStyle::OnlyConflicts.into();
assert_eq!(style, subversion_sys::svn_diff_conflict_display_style_t_svn_diff_conflict_display_only_conflicts);
}
#[test]
fn test_file_diff4() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original_path = temp_dir.path().join("original.txt");
let modified_path = temp_dir.path().join("modified.txt");
let latest_path = temp_dir.path().join("latest.txt");
let ancestor_path = temp_dir.path().join("ancestor.txt");
std::fs::write(&original_path, b"line 1\nline 2\nline 3\n")?;
std::fs::write(&modified_path, b"line 1 modified\nline 2\nline 3\n")?;
std::fs::write(&latest_path, b"line 1\nline 2 latest\nline 3\n")?;
std::fs::write(&ancestor_path, b"line 1\nline 2\nline 3\n")?;
let options = FileOptions::default();
let diff = file_diff4(
&original_path,
&modified_path,
&latest_path,
&ancestor_path,
options,
)?;
assert!(diff.contains_changes());
Ok(())
}
}