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<'a> {
ptr: *mut subversion_sys::svn_diff_hunk_t,
_phantom: std::marker::PhantomData<&'a PatchFile>,
}
impl DiffHunk<'_> {
unsafe fn from_raw(ptr: *mut subversion_sys::svn_diff_hunk_t) -> Self {
Self {
ptr,
_phantom: std::marker::PhantomData,
}
}
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() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatchOperation {
Unchanged,
Added,
Deleted,
Copied,
Moved,
}
impl From<subversion_sys::svn_diff_operation_kind_t> for PatchOperation {
fn from(kind: subversion_sys::svn_diff_operation_kind_t) -> Self {
match kind {
subversion_sys::svn_diff_operation_kind_e_svn_diff_op_unchanged => {
PatchOperation::Unchanged
}
subversion_sys::svn_diff_operation_kind_e_svn_diff_op_added => PatchOperation::Added,
subversion_sys::svn_diff_operation_kind_e_svn_diff_op_deleted => {
PatchOperation::Deleted
}
subversion_sys::svn_diff_operation_kind_e_svn_diff_op_copied => PatchOperation::Copied,
subversion_sys::svn_diff_operation_kind_e_svn_diff_op_moved => PatchOperation::Moved,
_ => unreachable!("unknown svn_diff_operation_kind_t value: {}", kind),
}
}
}
pub struct Patch<'a> {
ptr: *mut subversion_sys::svn_patch_t,
_phantom: std::marker::PhantomData<&'a PatchFile>,
}
impl Patch<'_> {
pub fn old_filename(&self) -> Option<&str> {
unsafe {
let p = (*self.ptr).old_filename;
if p.is_null() {
None
} else {
std::ffi::CStr::from_ptr(p).to_str().ok()
}
}
}
pub fn new_filename(&self) -> Option<&str> {
unsafe {
let p = (*self.ptr).new_filename;
if p.is_null() {
None
} else {
std::ffi::CStr::from_ptr(p).to_str().ok()
}
}
}
pub fn operation(&self) -> PatchOperation {
unsafe { (*self.ptr).operation.into() }
}
pub fn hunks(&self) -> Vec<DiffHunk<'_>> {
unsafe {
let array = (*self.ptr).hunks;
if array.is_null() {
return Vec::new();
}
let arr = &*array;
let elts = arr.elts as *const *mut subversion_sys::svn_diff_hunk_t;
(0..arr.nelts as isize)
.map(|i| DiffHunk::from_raw(*elts.offset(i)))
.collect()
}
}
}
pub struct PatchFile {
ptr: *mut subversion_sys::svn_patch_file_t,
pool: apr::Pool<'static>,
}
impl PatchFile {
pub fn open(path: &std::path::Path) -> Result<Self, Error<'static>> {
let pool = apr::Pool::new();
let abspath = crate::dirent::to_absolute_cstring(path.to_string_lossy().as_ref())?;
let mut ptr = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_diff_open_patch_file(&mut ptr, abspath.as_ptr(), pool.as_mut_ptr())
};
svn_result(err)?;
Ok(Self { ptr, pool })
}
pub fn next_patch(
&mut self,
reverse: bool,
ignore_whitespace: bool,
) -> Result<Option<Patch<'_>>, Error<'static>> {
let mut patch_ptr = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_diff_parse_next_patch(
&mut patch_ptr,
self.ptr,
reverse as subversion_sys::svn_boolean_t,
ignore_whitespace as subversion_sys::svn_boolean_t,
self.pool.as_mut_ptr(),
self.pool.as_mut_ptr(),
)
};
svn_result(err)?;
if patch_ptr.is_null() {
Ok(None)
} else {
Ok(Some(Patch {
ptr: patch_ptr,
_phantom: std::marker::PhantomData,
}))
}
}
}
impl Drop for PatchFile {
fn drop(&mut self) {
unsafe {
let err = subversion_sys::svn_diff_close_patch_file(self.ptr, self.pool.as_mut_ptr());
if !err.is_null() {
subversion_sys::svn_error_clear(err);
}
}
}
}
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_diff_options_builder() {
let options = DiffOptions::new()
.with_ignore_whitespace(true)
.with_ignore_eol_style(true)
.with_show_c_function(true);
assert!(options.ignore_whitespace);
assert!(options.ignore_eol_style);
assert!(options.show_c_function);
let only_ws = DiffOptions::new().with_ignore_whitespace(true);
assert!(only_ws.ignore_whitespace);
assert!(!only_ws.ignore_eol_style);
assert!(!only_ws.show_c_function);
let only_eol = DiffOptions::new().with_ignore_eol_style(true);
assert!(!only_eol.ignore_whitespace);
assert!(only_eol.ignore_eol_style);
assert!(!only_eol.show_c_function);
let only_cfn = DiffOptions::new().with_show_c_function(true);
assert!(!only_cfn.ignore_whitespace);
assert!(!only_cfn.ignore_eol_style);
assert!(only_cfn.show_c_function);
}
#[test]
fn test_ignore_space_into_raw() {
use subversion_sys::*;
assert_eq!(
svn_diff_file_ignore_space_t::from(IgnoreSpace::None),
svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_none
);
assert_eq!(
svn_diff_file_ignore_space_t::from(IgnoreSpace::Change),
svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_change
);
assert_eq!(
svn_diff_file_ignore_space_t::from(IgnoreSpace::All),
svn_diff_file_ignore_space_t_svn_diff_file_ignore_space_all
);
}
#[test]
fn test_file_diff3_conflict() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let original = temp_dir.path().join("orig.txt");
let modified = temp_dir.path().join("mine.txt");
let latest = temp_dir.path().join("theirs.txt");
std::fs::write(&original, b"line 1\nline 2\nline 3\n")?;
std::fs::write(&modified, b"line 1\nMINE\nline 3\n")?;
std::fs::write(&latest, b"line 1\nTHEIRS\nline 3\n")?;
let diff = file_diff3(&original, &modified, &latest, FileOptions::default())?;
assert!(diff.contains_changes());
assert!(diff.contains_conflicts());
Ok(())
}
#[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(())
}
#[test]
fn test_parse_patch_file_and_hunks() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempdir()?;
let patch_path = temp_dir.path().join("change.patch");
let patch = "--- a/file.txt\n+++ b/file.txt\n@@ -5,4 +8,5 @@\n c1\n c2\n-old\n+new1\n+new2\n c3\n c4\n";
std::fs::write(&patch_path, patch)?;
let mut patch_file = PatchFile::open(&patch_path)?;
let patch = patch_file
.next_patch(false, false)?
.expect("expected one patch");
assert_eq!(patch.old_filename(), Some("a/file.txt"));
assert_eq!(patch.new_filename(), Some("b/file.txt"));
assert_eq!(patch.operation(), PatchOperation::Unchanged);
let hunks = patch.hunks();
assert_eq!(hunks.len(), 1);
let hunk = &hunks[0];
assert_eq!(hunk.original_start(), 5);
assert_eq!(hunk.original_length(), 5);
assert_eq!(hunk.modified_start(), 8);
assert_eq!(hunk.modified_length(), 6);
assert_eq!(hunk.leading_context(), 2);
assert_eq!(hunk.trailing_context(), 2);
assert!(patch_file.next_patch(false, false)?.is_none());
Ok(())
}
#[test]
fn test_patch_operation_from() {
use subversion_sys::*;
assert_eq!(
PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_unchanged),
PatchOperation::Unchanged
);
assert_eq!(
PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_added),
PatchOperation::Added
);
assert_eq!(
PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_deleted),
PatchOperation::Deleted
);
assert_eq!(
PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_copied),
PatchOperation::Copied
);
assert_eq!(
PatchOperation::from(svn_diff_operation_kind_e_svn_diff_op_moved),
PatchOperation::Moved
);
}
}