use crate::{svn_result, with_tmp_pool, Depth, Error, OwnedLogEntry, Revnum};
use apr::pool::Pool;
use std::collections::HashMap;
use std::marker::PhantomData;
use subversion_sys::svn_ra_session_t;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RelPath(String);
impl RelPath {
pub fn canonicalize(path: &str) -> Result<Self, Error<'static>> {
let path = path.strip_prefix('/').unwrap_or(path);
with_tmp_pool(|pool| unsafe {
let path_cstr = std::ffi::CString::new(path)?;
let canonical =
subversion_sys::svn_relpath_canonicalize(path_cstr.as_ptr(), pool.as_mut_ptr());
let canonical_cstr = std::ffi::CStr::from_ptr(canonical);
Ok(RelPath(canonical_cstr.to_str()?.to_owned()))
})
}
pub fn from_canonical(path: String) -> Result<Self, Error<'static>> {
let is_canonical = with_tmp_pool(|_pool| unsafe {
let path_cstr = std::ffi::CString::new(path.as_str())?;
Ok::<bool, Error>(subversion_sys::svn_relpath_is_canonical(path_cstr.as_ptr()) != 0)
})?;
if !is_canonical {
return Err(Error::from_message(&format!(
"Relative path is not canonical: {}",
path
)));
}
Ok(RelPath(path))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for RelPath {
type Error = Error<'static>;
fn try_from(path: &str) -> Result<Self, Self::Error> {
Self::from_canonical(path.to_string())
}
}
impl TryFrom<String> for RelPath {
type Error = Error<'static>;
fn try_from(path: String) -> Result<Self, Self::Error> {
Self::from_canonical(path)
}
}
impl TryFrom<&String> for RelPath {
type Error = Error<'static>;
fn try_from(path: &String) -> Result<Self, Self::Error> {
Self::from_canonical(path.clone())
}
}
impl<'a> TryFrom<&'a &str> for RelPath {
type Error = Error<'static>;
fn try_from(path: &'a &str) -> Result<Self, Self::Error> {
Self::from_canonical((*path).to_string())
}
}
impl std::fmt::Display for RelPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
#[derive(Debug, Clone)]
pub struct RepositoryInfo {
pub uuid: String,
pub root_url: String,
pub latest_revision: Revnum,
pub session_url: String,
}
pub use crate::DirEntry as Dirent;
pub struct Session<'a> {
ptr: *mut svn_ra_session_t,
_pool: apr::Pool<'static>,
_callbacks: Option<&'a mut Callbacks>,
_owned_callbacks: Option<Box<Callbacks>>,
_phantom: PhantomData<*mut ()>, }
impl<'a> Drop for Session<'a> {
fn drop(&mut self) {
}
}
pub(crate) extern "C" fn wrap_dirent_receiver(
rel_path: *const std::os::raw::c_char,
dirent: *mut subversion_sys::svn_dirent_t,
baton: *mut std::os::raw::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let rel_path = unsafe { std::ffi::CStr::from_ptr(rel_path) };
let callback = unsafe {
&*(baton as *const _ as *const &dyn Fn(&str, &Dirent) -> Result<(), crate::Error<'static>>)
};
match callback(rel_path.to_str().unwrap(), &Dirent::from_raw(dirent)) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => e.as_mut_ptr(),
}
}
extern "C" fn wrap_location_segment_receiver(
svn_location_segment: *mut subversion_sys::svn_location_segment_t,
baton: *mut std::os::raw::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = unsafe {
&*(baton as *const _
as *const &dyn Fn(&crate::LocationSegment) -> Result<(), crate::Error<'static>>)
};
match baton(&crate::LocationSegment {
ptr: svn_location_segment,
_pool: std::marker::PhantomData,
}) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => e.as_mut_ptr(),
}
}
extern "C" fn wrap_lock_func(
lock_baton: *mut std::os::raw::c_void,
path: *const std::os::raw::c_char,
do_lock: i32,
lock: *const subversion_sys::svn_lock_t,
error: *mut subversion_sys::svn_error_t,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let lock_baton = unsafe {
&mut **(lock_baton
as *mut &mut dyn FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>)
};
let path = unsafe { std::ffi::CStr::from_ptr(path) };
let error = Error::from_raw(error).err();
let lock_obj = if lock.is_null() {
None
} else {
let pool_handle = unsafe { apr::PoolHandle::from_borrowed_raw(pool) };
Some(crate::Lock::from_raw(lock as *mut _, pool_handle))
};
match lock_baton(
path.to_str().unwrap(),
do_lock != 0,
lock_obj.as_ref(),
error.as_ref(),
) {
Ok(()) => std::ptr::null_mut(),
Err(e) => unsafe { e.into_raw() },
}
}
pub struct DoDiffOptions<'a> {
pub depth: crate::Depth,
pub ignore_ancestry: bool,
pub text_deltas: bool,
pub versus_url: &'a str,
pub diff_editor: &'a mut crate::delta::WrapEditor<'a>,
}
impl<'a> DoDiffOptions<'a> {
pub fn new(versus_url: &'a str, diff_editor: &'a mut crate::delta::WrapEditor<'a>) -> Self {
Self {
depth: crate::Depth::Infinity,
ignore_ancestry: false,
text_deltas: true,
versus_url,
diff_editor,
}
}
pub fn with_depth(mut self, depth: crate::Depth) -> Self {
self.depth = depth;
self
}
pub fn with_ignore_ancestry(mut self, ignore_ancestry: bool) -> Self {
self.ignore_ancestry = ignore_ancestry;
self
}
pub fn with_text_deltas(mut self, text_deltas: bool) -> Self {
self.text_deltas = text_deltas;
self
}
}
#[derive(Default)]
pub struct GetLogOptions<'a> {
pub limit: usize,
pub discover_changed_paths: bool,
pub strict_node_history: bool,
pub include_merged_revisions: bool,
pub revprops: Option<&'a [&'a str]>,
}
impl<'a> GetLogOptions<'a> {
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
pub fn with_discover_changed_paths(mut self, discover: bool) -> Self {
self.discover_changed_paths = discover;
self
}
pub fn with_strict_node_history(mut self, strict: bool) -> Self {
self.strict_node_history = strict;
self
}
pub fn with_include_merged_revisions(mut self, include: bool) -> Self {
self.include_merged_revisions = include;
self
}
pub fn with_revprops(mut self, revprops: Option<&'a [&'a str]>) -> Self {
self.revprops = revprops;
self
}
}
pub struct RaLogIterator<'a> {
rx: std::sync::mpsc::Receiver<Result<OwnedLogEntry, Error<'static>>>,
handle: Option<std::thread::JoinHandle<()>>,
_phantom: PhantomData<&'a mut Session<'a>>,
}
impl Iterator for RaLogIterator<'_> {
type Item = Result<OwnedLogEntry, Error<'static>>;
fn next(&mut self) -> Option<Self::Item> {
self.rx.recv().ok()
}
}
impl Drop for RaLogIterator<'_> {
fn drop(&mut self) {
drop(std::mem::replace(
&mut self.rx,
std::sync::mpsc::sync_channel(0).1,
));
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
impl<'a> Session<'a> {
#[cfg(feature = "client")]
pub(crate) unsafe fn from_ptr_and_pool(
ptr: *mut svn_ra_session_t,
pool: apr::Pool<'static>,
) -> Self {
Self {
ptr,
_pool: pool,
_callbacks: None,
_owned_callbacks: None,
_phantom: PhantomData,
}
}
pub fn as_ptr(&self) -> *const svn_ra_session_t {
self.ptr
}
pub fn as_mut_ptr(&mut self) -> *mut svn_ra_session_t {
self.ptr
}
pub fn open(
url: &str,
uuid: Option<&str>,
mut callbacks: Option<&'a mut Callbacks>,
mut config: Option<&mut crate::config::ConfigHash>,
) -> Result<(Self, Option<String>, Option<String>), Error<'static>> {
crate::init::initialize()?;
let url = crate::uri::canonicalize_uri(url)?;
let url = std::ffi::CString::new(url.as_str()).unwrap();
let mut corrected_url = std::ptr::null();
let mut redirect_url = std::ptr::null();
let pool = Pool::new();
let mut session = std::ptr::null_mut();
let uuid = uuid.map(|uuid| std::ffi::CString::new(uuid).unwrap());
let mut owned_callbacks = None;
let (callbacks_ptr, callback_baton) = if let Some(callbacks) = callbacks.as_mut() {
(callbacks.as_mut_ptr(), callbacks.get_callback_baton())
} else {
let mut default_callbacks = Box::new(Callbacks::new()?);
let ptr = default_callbacks.as_mut_ptr();
let baton = default_callbacks.get_callback_baton();
owned_callbacks = Some(default_callbacks);
(ptr, baton)
};
let err = unsafe {
subversion_sys::svn_ra_open5(
&mut session,
&mut corrected_url,
&mut redirect_url,
url.as_ptr(),
if let Some(uuid) = uuid {
uuid.as_ptr()
} else {
std::ptr::null()
},
callbacks_ptr,
callback_baton,
if let Some(config) = config.as_mut() {
config.as_mut_ptr()
} else {
std::ptr::null_mut()
},
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok((
Session {
ptr: session,
_pool: pool,
_callbacks: callbacks,
_owned_callbacks: owned_callbacks,
_phantom: PhantomData,
},
if corrected_url.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(corrected_url) }
.to_str()
.unwrap()
.to_string(),
)
},
if redirect_url.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(redirect_url) }
.to_str()
.unwrap()
.to_string(),
)
},
))
}
pub fn reparent(&mut self, url: &str) -> Result<(), Error<'static>> {
let url = crate::uri::canonicalize_uri(url)?;
let url = std::ffi::CString::new(url.as_str()).unwrap();
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_ra_reparent(self.ptr, url.as_ptr(), pool.as_mut_ptr())
};
Error::from_raw(err)
})
}
pub fn get_session_url(&mut self) -> Result<String, Error<'static>> {
with_tmp_pool(|pool| {
let mut url = std::ptr::null();
let err = unsafe {
subversion_sys::svn_ra_get_session_url(self.ptr, &mut url, pool.as_mut_ptr())
};
Error::from_raw(err)?;
let url = unsafe { std::ffi::CStr::from_ptr(url) };
Ok(url.to_string_lossy().into_owned())
})
}
pub fn get_path_relative_to_session(&mut self, url: &str) -> Result<String, Error<'static>> {
let url = std::ffi::CString::new(url).unwrap();
let pool = Pool::new();
let mut path = std::ptr::null();
let err = unsafe {
subversion_sys::svn_ra_get_path_relative_to_session(
self.ptr,
&mut path,
url.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let path = unsafe { std::ffi::CStr::from_ptr(path) };
Ok(path.to_string_lossy().into_owned())
}
pub fn get_path_relative_to_root(&mut self, url: &str) -> String {
let url = std::ffi::CString::new(url).unwrap();
let pool = Pool::new();
let mut path = std::ptr::null();
unsafe {
subversion_sys::svn_ra_get_path_relative_to_root(
self.ptr,
&mut path,
url.as_ptr(),
pool.as_mut_ptr(),
);
}
let path = unsafe { std::ffi::CStr::from_ptr(path) };
path.to_string_lossy().into_owned()
}
pub fn get_latest_revnum(&mut self) -> Result<Revnum, Error<'static>> {
with_tmp_pool(|pool| {
let mut revnum = 0;
let err = unsafe {
subversion_sys::svn_ra_get_latest_revnum(self.ptr, &mut revnum, pool.as_mut_ptr())
};
Error::from_raw(err)?;
Ok(Revnum::from_raw(revnum).unwrap())
})
}
pub fn get_dated_revision(&mut self, tm: apr::apr_time_t) -> Result<Revnum, Error<'static>> {
with_tmp_pool(|pool| {
let mut revnum = 0;
let err = unsafe {
subversion_sys::svn_ra_get_dated_revision(
self.ptr,
&mut revnum,
tm,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Revnum::from_raw(revnum).unwrap())
})
}
pub fn change_revprop(
&mut self,
rev: Revnum,
name: &str,
old_value: Option<&[u8]>,
new_value: &[u8],
) -> Result<(), Error<'static>> {
let name = std::ffi::CString::new(name).unwrap();
let pool = Pool::new();
let new_value = subversion_sys::svn_string_t {
data: new_value.as_ptr() as *mut _,
len: new_value.len(),
};
let old_value = old_value.map(|v| subversion_sys::svn_string_t {
data: v.as_ptr() as *mut _,
len: v.len(),
});
let err = unsafe {
subversion_sys::svn_ra_change_rev_prop2(
self.ptr,
rev.into(),
name.as_ptr(),
&old_value.map_or(std::ptr::null(), |v| &v),
&new_value,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn rev_proplist(&mut self, rev: Revnum) -> Result<HashMap<String, Vec<u8>>, Error<'_>> {
if rev.0 < 0 {
return Err(Error::with_raw_status(
subversion_sys::svn_errno_t_SVN_ERR_CLIENT_BAD_REVISION as i32,
None,
"Invalid revision number",
));
}
let pool = Pool::new();
let mut props = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_ra_rev_proplist(self.ptr, rev.into(), &mut props, pool.as_mut_ptr())
};
Error::from_raw(err)?;
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
Ok(prop_hash.to_hashmap())
}
pub fn rev_prop(&mut self, rev: Revnum, name: &str) -> Result<Option<Vec<u8>>, Error<'_>> {
let name = std::ffi::CString::new(name).unwrap();
let pool = Pool::new();
let mut value = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_ra_rev_prop(
self.ptr,
rev.into(),
name.as_ptr(),
&mut value,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if value.is_null() {
Ok(None)
} else {
Ok(Some(Vec::from(unsafe {
std::slice::from_raw_parts((*value).data as *const u8, (*value).len)
})))
}
}
pub fn get_commit_editor<'s>(
&'s mut self,
revprop_table: HashMap<String, Vec<u8>>,
commit_callback: &dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
lock_tokens: HashMap<String, String>,
keep_locks: bool,
) -> Result<crate::delta::WrapEditor<'s>, Error<'s>> {
let result_pool = Pool::new();
let commit_callback = Box::into_raw(Box::new(commit_callback));
let mut editor = std::ptr::null();
let mut edit_baton = std::ptr::null_mut();
let svn_strings_and_keys: Vec<_> = revprop_table
.iter()
.map(|(k, v)| {
let key_cstr = std::ffi::CString::new(k.as_str()).unwrap();
let key_in_pool =
unsafe { apr_sys::apr_pstrdup(result_pool.as_mut_ptr(), key_cstr.as_ptr()) };
let key_slice = unsafe { std::ffi::CStr::from_ptr(key_in_pool).to_bytes() };
(
key_slice,
crate::string::BStr::from_bytes(v.as_slice(), &result_pool),
)
})
.collect();
let mut hash_revprop_table = apr::hash::Hash::new(&result_pool);
for (key_slice, v) in svn_strings_and_keys.iter() {
unsafe {
hash_revprop_table.insert(key_slice, v.as_ptr() as *mut std::ffi::c_void);
}
}
let c_strings: Vec<_> = lock_tokens
.iter()
.map(|(k, v)| {
let key_cstr = std::ffi::CString::new(k.as_str()).unwrap();
let value_cstr = std::ffi::CString::new(v.as_str()).unwrap();
let key_in_pool =
unsafe { apr_sys::apr_pstrdup(result_pool.as_mut_ptr(), key_cstr.as_ptr()) };
let key_slice = unsafe { std::ffi::CStr::from_ptr(key_in_pool).to_bytes() };
let value_in_pool =
unsafe { apr_sys::apr_pstrdup(result_pool.as_mut_ptr(), value_cstr.as_ptr()) };
(key_slice, value_in_pool)
})
.collect();
let mut hash_lock_tokens = apr::hash::Hash::new(&result_pool);
for (k, v) in c_strings.iter() {
unsafe {
hash_lock_tokens.insert(k, *v as *mut std::ffi::c_void);
}
}
let err = unsafe {
subversion_sys::svn_ra_get_commit_editor3(
self.ptr,
&mut editor,
&mut edit_baton,
hash_revprop_table.as_mut_ptr(),
Some(crate::wrap_commit_callback2),
commit_callback as *mut _ as *mut _,
hash_lock_tokens.as_mut_ptr(),
keep_locks.into(),
result_pool.as_mut_ptr(),
)
};
unsafe fn drop_commit_callback_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton as *mut &dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
));
}
Error::from_raw(err)?;
Ok(crate::delta::WrapEditor {
editor,
baton: edit_baton,
_pool: apr::PoolHandle::owned(result_pool),
callback_batons: vec![(
commit_callback as *mut std::ffi::c_void,
drop_commit_callback_baton,
)],
})
}
pub fn get_file(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
rev: Revnum,
stream: &mut crate::io::Stream,
) -> Result<(Option<Revnum>, HashMap<String, Vec<u8>>), Error<'static>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let pool = Pool::new();
let mut props = std::ptr::null_mut();
let mut fetched_rev = 0;
let err = unsafe {
subversion_sys::svn_ra_get_file(
self.ptr,
path.as_ptr(),
rev.into(),
stream.as_mut_ptr(),
&mut fetched_rev,
&mut props,
pool.as_mut_ptr(),
)
};
crate::Error::from_raw(err)?;
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
let actual_fetched_rev = if fetched_rev == 0 {
None
} else {
Revnum::from_raw(fetched_rev)
};
Ok((actual_fetched_rev, prop_hash.to_hashmap()))
}
pub fn get_dir(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
rev: Revnum,
dirent_fields: crate::DirentField,
) -> Result<(Revnum, HashMap<String, Dirent>, HashMap<String, Vec<u8>>), Error<'_>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let pool = Pool::new();
let mut props = std::ptr::null_mut();
let mut fetched_rev = rev.0;
let mut dirents = std::ptr::null_mut();
let dirent_fields = dirent_fields.bits();
let err = unsafe {
subversion_sys::svn_ra_get_dir2(
self.ptr,
&mut dirents,
&mut fetched_rev,
&mut props,
path.as_ptr(),
rev.into(),
dirent_fields,
pool.as_mut_ptr(),
)
};
crate::Error::from_raw(err)?;
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
let dirents_hash = unsafe { crate::hash::DirentHash::from_ptr(dirents) };
let props = prop_hash.to_hashmap();
let dirents = dirents_hash.to_hashmap();
Ok((Revnum::from_raw(fetched_rev).unwrap(), dirents, props))
}
pub fn list(
&mut self,
path: &str,
rev: Revnum,
patterns: Option<&[&str]>,
depth: Depth,
dirent_fields: crate::DirentField,
dirent_receiver: &dyn Fn(&str, &Dirent) -> Result<(), crate::Error<'static>>,
) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path).unwrap();
let pool = Pool::new();
let pattern_cstrings: Option<Vec<std::ffi::CString>> = patterns.map(|patterns| {
patterns
.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect()
});
let patterns_array: Option<apr::tables::TypedArray<*const std::os::raw::c_char>> =
pattern_cstrings.as_ref().map(|cstrings| {
let mut array = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
&pool,
cstrings.len() as i32,
);
for cstring in cstrings {
array.push(cstring.as_ptr());
}
array
});
let dirent_fields = dirent_fields.bits();
let err = unsafe {
subversion_sys::svn_ra_list(
self.ptr,
path.as_ptr(),
rev.into(),
if let Some(array) = patterns_array.as_ref() {
array.as_ptr()
} else {
std::ptr::null()
},
depth.into(),
dirent_fields,
Some(wrap_dirent_receiver),
&dirent_receiver as *const _ as *mut _,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn get_mergeinfo(
&mut self,
paths: &[&str],
revision: Revnum,
inherit: crate::mergeinfo::MergeinfoInheritance,
include_descendants: bool,
) -> Result<HashMap<String, crate::mergeinfo::Mergeinfo>, Error<'_>> {
let pool = Pool::new();
let c_paths: Vec<std::ffi::CString> = paths
.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect();
let mut paths_array =
apr::tables::TypedArray::<*const std::os::raw::c_char>::new(&pool, paths.len() as i32);
for c_path in &c_paths {
paths_array.push(c_path.as_ptr());
}
let paths = paths_array;
let mut mergeinfo = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_ra_get_mergeinfo(
self.ptr,
&mut mergeinfo,
paths.as_ptr(),
revision.into(),
inherit.into(),
include_descendants.into(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if mergeinfo.is_null() {
return Ok(HashMap::new());
}
let mergeinfo = unsafe { apr::hash::Hash::from_ptr(mergeinfo) };
Ok(mergeinfo
.iter()
.map(|(k, v)| {
(String::from_utf8_lossy(k).into_owned(), unsafe {
crate::mergeinfo::Mergeinfo::from_ptr_and_pool(
v as *mut apr_sys::apr_hash_t,
apr::Pool::new(),
)
})
})
.collect())
}
pub fn do_update<'s>(
&'s mut self,
revision_to_update_to: Revnum,
update_target: &str,
depth: Depth,
send_copyfrom_args: bool,
ignore_ancestry: bool,
editor: &mut crate::delta::WrapEditor,
) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
let update_target = std::ffi::CString::new(update_target)?;
let pool = Pool::new();
let scratch_pool = Pool::new();
let mut reporter = std::ptr::null();
let mut report_baton = std::ptr::null_mut();
let (editor_ptr, editor_baton) = editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_ra_do_update3(
self.ptr,
&mut reporter,
&mut report_baton,
revision_to_update_to.into(),
update_target.as_ptr(),
depth.into(),
send_copyfrom_args.into(),
ignore_ancestry.into(),
editor_ptr,
editor_baton,
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Box::new(WrapReporter {
reporter,
baton: report_baton,
_pool: pool,
callback_batons: Vec::new(),
_phantom: PhantomData,
}) as Box<dyn Reporter + Send>)
}
pub fn do_switch<'s>(
&'s mut self,
revision_to_switch_to: Revnum,
switch_target: &str,
depth: Depth,
switch_url: &str,
send_copyfrom_args: bool,
ignore_ancestry: bool,
editor: &mut crate::delta::WrapEditor,
) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
let switch_target = std::ffi::CString::new(switch_target)?;
let switch_url = crate::uri::canonicalize_uri(switch_url)?;
let switch_url = std::ffi::CString::new(switch_url.as_str())?;
let pool = Pool::new();
let scratch_pool = Pool::new();
let mut reporter = std::ptr::null();
let mut report_baton = std::ptr::null_mut();
let (editor_ptr, editor_baton) = editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_ra_do_switch3(
self.ptr,
&mut reporter,
&mut report_baton,
revision_to_switch_to.into(),
switch_target.as_ptr(),
depth.into(),
switch_url.as_ptr(),
send_copyfrom_args.into(),
ignore_ancestry.into(),
editor_ptr,
editor_baton,
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Box::new(WrapReporter {
reporter,
baton: report_baton,
_pool: pool,
callback_batons: Vec::new(),
_phantom: PhantomData,
}) as Box<dyn Reporter + Send>)
}
pub fn check_path(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
rev: Revnum,
) -> Result<crate::NodeKind, Error<'static>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let mut kind = 0;
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_check_path(
self.ptr,
path.as_ptr(),
rev.into(),
&mut kind,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(crate::NodeKind::from(kind))
}
pub fn path_exists(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
rev: Revnum,
) -> Result<bool, Error<'static>> {
let kind = self.check_path(path, rev)?;
Ok(!matches!(kind, crate::NodeKind::None))
}
pub fn do_status(
&mut self,
status_target: impl TryInto<RelPath, Error = Error<'static>>,
revision: Revnum,
depth: Depth,
status_editor: &mut crate::delta::WrapEditor,
) -> Result<(), Error<'static>> {
let relpath = status_target.try_into()?;
let status_target = std::ffi::CString::new(relpath.as_str()).unwrap();
let pool = Pool::new();
let mut reporter = std::ptr::null();
let mut report_baton = std::ptr::null_mut();
let (editor_ptr, editor_baton) = status_editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_ra_do_status2(
self.ptr,
&mut reporter,
&mut report_baton,
status_target.as_ptr() as *const _,
revision.into(),
depth.into(),
editor_ptr,
editor_baton,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn stat(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
rev: Revnum,
) -> Result<Dirent, Error<'static>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let mut dirent = std::ptr::null_mut();
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_stat(
self.ptr,
path.as_ptr(),
rev.into(),
&mut dirent,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Dirent::from_raw(dirent))
}
pub fn get_uuid(&mut self) -> Result<String, Error<'static>> {
let pool = Pool::new();
let mut uuid = std::ptr::null();
let err =
unsafe { subversion_sys::svn_ra_get_uuid2(self.ptr, &mut uuid, pool.as_mut_ptr()) };
Error::from_raw(err)?;
let uuid = unsafe { std::ffi::CStr::from_ptr(uuid) };
Ok(uuid.to_string_lossy().into_owned())
}
pub fn get_repos_root(&mut self) -> Result<String, Error<'static>> {
with_tmp_pool(|pool| {
let mut url = std::ptr::null();
let err = unsafe {
subversion_sys::svn_ra_get_repos_root2(self.ptr, &mut url, pool.as_mut_ptr())
};
Error::from_raw(err)?;
let url = unsafe { std::ffi::CStr::from_ptr(url) };
Ok(url.to_str().unwrap().to_string())
})
}
pub fn get_repository_info(&mut self) -> Result<RepositoryInfo, Error<'static>> {
Ok(RepositoryInfo {
uuid: self.get_uuid()?,
root_url: self.get_repos_root()?,
latest_revision: self.get_latest_revnum()?,
session_url: self.get_session_url()?,
})
}
pub fn get_deleted_rev(
&mut self,
path: &str,
peg_revision: Revnum,
end_revision: Revnum,
) -> Result<Revnum, Error<'static>> {
let path = std::ffi::CString::new(path).unwrap();
let mut rev = 0;
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_get_deleted_rev(
self.ptr,
path.as_ptr(),
peg_revision.into(),
end_revision.into(),
&mut rev,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Revnum::from_raw(rev).unwrap())
}
pub fn has_capability(&mut self, capability: &str) -> Result<bool, Error<'static>> {
let capability = std::ffi::CString::new(capability).unwrap();
let mut has = 0;
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_has_capability(
self.ptr,
&mut has,
capability.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(has != 0)
}
pub fn get_files(
&mut self,
paths: &[&str],
rev: Revnum,
) -> Result<Vec<(String, Vec<u8>, HashMap<String, Vec<u8>>)>, Error<'static>> {
let mut results = Vec::new();
for path in paths {
let mut stringbuf = crate::io::StringBuf::new();
let mut stream = crate::io::Stream::from_stringbuf(&mut stringbuf);
let (_fetched_rev, props) = self.get_file(path, rev, &mut stream)?;
let contents = stringbuf.as_bytes().to_vec();
results.push((path.to_string(), contents, props));
}
Ok(results)
}
pub fn diff<'s>(
&'s mut self,
revision: Revnum,
diff_target: impl TryInto<RelPath, Error = Error<'static>>,
depth: Depth,
ignore_ancestry: bool,
text_deltas: bool,
versus_url: &str,
diff_editor: &mut crate::delta::WrapEditor,
) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
let relpath = diff_target.try_into()?;
let diff_target = std::ffi::CString::new(relpath.as_str()).unwrap();
let versus_url = std::ffi::CString::new(versus_url).unwrap();
let pool = Pool::new();
let mut reporter = std::ptr::null();
let mut report_baton = std::ptr::null_mut();
let (editor_ptr, editor_baton) = diff_editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_ra_do_diff3(
self.ptr,
&mut reporter,
&mut report_baton,
revision.into(),
diff_target.as_ptr() as *const _,
depth.into(),
ignore_ancestry.into(),
text_deltas.into(),
versus_url.as_ptr() as *const _,
editor_ptr,
editor_baton,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Box::new(WrapReporter {
reporter,
baton: report_baton,
_pool: pool,
callback_batons: Vec::new(),
_phantom: PhantomData,
}) as Box<dyn Reporter + Send>)
}
pub fn get_log(
&mut self,
paths: &[&str],
start: Revnum,
end: Revnum,
options: &GetLogOptions,
log_receiver: &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let path_cstrings: Vec<std::ffi::CString> = paths
.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect();
let mut paths_array =
apr::tables::TypedArray::<*const std::os::raw::c_char>::new(&pool, paths.len() as i32);
for cstr in &path_cstrings {
paths_array.push(cstr.as_ptr());
}
let revprop_cstrings: Vec<std::ffi::CString> = options
.revprops
.map(|rp| {
rp.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect()
})
.unwrap_or_default();
let mut revprops_array = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
&pool,
revprop_cstrings.len() as i32,
);
for cstr in &revprop_cstrings {
revprops_array.push(cstr.as_ptr());
}
let revprops_ptr = if options.revprops.is_some() {
unsafe { revprops_array.as_ptr() }
} else {
std::ptr::null()
};
let baton = Box::into_raw(Box::new(log_receiver)) as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_ra_get_log2(
self.ptr,
paths_array.as_ptr(),
start.into(),
end.into(),
options.limit as _,
options.discover_changed_paths.into(),
options.strict_node_history.into(),
options.include_merged_revisions.into(),
revprops_ptr,
Some(crate::wrap_log_entry_receiver),
baton,
pool.as_mut_ptr(),
)
};
let _ = unsafe {
Box::from_raw(
baton as *mut &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn iter_logs(
&mut self,
paths: &[&str],
start: Revnum,
end: Revnum,
options: &GetLogOptions,
) -> RaLogIterator<'_> {
let (tx, rx) = std::sync::mpsc::sync_channel::<Result<OwnedLogEntry, Error<'static>>>(4);
let paths: Vec<String> = paths.iter().map(|s| s.to_string()).collect();
let limit = options.limit;
let discover_changed_paths = options.discover_changed_paths;
let strict_node_history = options.strict_node_history;
let include_merged_revisions = options.include_merged_revisions;
let revprops: Option<Vec<String>> = options
.revprops
.map(|rp| rp.iter().map(|s| s.to_string()).collect());
let session_addr = self as *mut Session as usize;
let handle = std::thread::spawn(move || {
let session = unsafe { &mut *(session_addr as *mut Session) };
let path_refs: Vec<&str> = paths.iter().map(|s| s.as_str()).collect();
let revprop_refs: Option<Vec<&str>> = revprops
.as_ref()
.map(|rp| rp.iter().map(|s| s.as_str()).collect());
let opts = GetLogOptions {
limit,
discover_changed_paths,
strict_node_history,
include_merged_revisions,
revprops: revprop_refs.as_deref(),
};
let result = session.get_log(&path_refs, start, end, &opts, &mut |entry| {
let owned = OwnedLogEntry::from_log_entry(entry);
if tx.send(Ok(owned)).is_err() {
return Err(Error::with_raw_status(
subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32,
None,
"Log iteration cancelled",
));
}
Ok(())
});
if let Err(e) = result {
if e.raw_apr_err() != subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32 {
let _ = tx.send(Err(e));
}
}
});
RaLogIterator {
rx,
handle: Some(handle),
_phantom: PhantomData,
}
}
pub fn get_locations(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
peg_revision: Revnum,
location_revisions: &[Revnum],
) -> Result<HashMap<Revnum, String>, Error<'_>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let pool = Pool::new();
let mut location_revisions_array =
apr::tables::TypedArray::<subversion_sys::svn_revnum_t>::new(
&pool,
location_revisions.len() as i32,
);
for rev in location_revisions {
location_revisions_array.push((*rev).into());
}
let location_revisions = location_revisions_array;
let mut locations = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_ra_get_locations(
self.ptr,
&mut locations,
path.as_ptr(),
peg_revision.into(),
location_revisions.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let iter = unsafe { apr::hash::Hash::from_ptr(locations) };
let mut locations = HashMap::new();
for (k, v) in iter.iter() {
let revnum = unsafe { *(k.as_ptr() as *const subversion_sys::svn_revnum_t) };
locations.insert(Revnum(revnum), unsafe {
std::ffi::CStr::from_ptr(v as *const std::ffi::c_char)
.to_string_lossy()
.into_owned()
});
}
Ok(locations)
}
pub fn get_location_segments(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
peg_revision: Revnum,
start: Revnum,
end: Revnum,
location_receiver: &dyn Fn(&crate::LocationSegment) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_get_location_segments(
self.ptr,
path.as_ptr(),
peg_revision.into(),
start.into(),
end.into(),
Some(wrap_location_segment_receiver),
&location_receiver as *const _ as *mut _,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn lock(
&mut self,
path_revs: &HashMap<String, Revnum>,
comment: &str,
steal_lock: bool,
mut lock_func: impl FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let scratch_pool = std::rc::Rc::new(Pool::new());
let revnum_values: Vec<_> = path_revs.values().map(|v| v.0).collect();
let path_cstrings: Vec<std::ffi::CString> = path_revs
.keys()
.map(|k| std::ffi::CString::new(k.as_str()).unwrap())
.collect();
let mut hash = apr::hash::Hash::new(&scratch_pool);
for (cstring, revnum_val) in path_cstrings.iter().zip(revnum_values.iter()) {
unsafe {
hash.insert(
cstring.as_bytes_with_nul(),
revnum_val as *const _ as *mut std::ffi::c_void,
);
}
}
let comment = std::ffi::CString::new(comment).unwrap();
let baton = Box::into_raw(Box::new(
&mut lock_func
as &mut dyn FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>,
)) as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_ra_lock(
self.ptr,
hash.as_mut_ptr(),
comment.as_ptr(),
steal_lock.into(),
Some(wrap_lock_func),
baton,
pool.as_mut_ptr(),
)
};
let _ = unsafe {
Box::from_raw(
baton
as *mut &mut dyn FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>,
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn unlock(
&mut self,
path_tokens: &HashMap<String, String>,
break_lock: bool,
mut lock_func: impl FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let scratch_pool = std::rc::Rc::new(Pool::new());
let mut hash = apr::hash::Hash::new(&scratch_pool);
let path_cstrings: Vec<std::ffi::CString> = path_tokens
.keys()
.map(|k| std::ffi::CString::new(k.as_str()).unwrap())
.collect();
let token_cstrings: Vec<std::ffi::CString> = path_tokens
.values()
.map(|v| std::ffi::CString::new(v.as_str()).unwrap())
.collect();
for (path_cstring, token_cstring) in path_cstrings.iter().zip(token_cstrings.iter()) {
unsafe {
hash.insert(
path_cstring.as_bytes_with_nul(),
token_cstring.as_ptr() as *mut std::ffi::c_void,
);
}
}
let baton = Box::into_raw(Box::new(
&mut lock_func
as &mut dyn FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>,
)) as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_ra_unlock(
self.ptr,
hash.as_mut_ptr(),
break_lock.into(),
Some(wrap_lock_func),
baton,
pool.as_mut_ptr(),
)
};
let _ = unsafe {
Box::from_raw(
baton
as *mut &mut dyn FnMut(
&str,
bool,
Option<&crate::Lock>,
Option<&Error>,
) -> Result<(), Error<'static>>,
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn get_lock(
&self,
path: impl TryInto<RelPath, Error = Error<'static>>,
) -> Result<Option<crate::Lock<'_>>, Error<'_>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let mut lock = std::ptr::null_mut();
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_get_lock(self.ptr, &mut lock, path.as_ptr(), pool.as_mut_ptr())
};
Error::from_raw(err)?;
if lock.is_null() {
Ok(None)
} else {
let pool_handle = apr::PoolHandle::owned(pool);
Ok(Some(crate::Lock::from_raw(lock, pool_handle)))
}
}
pub fn get_locks(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
depth: Depth,
) -> Result<HashMap<String, crate::Lock<'_>>, Error<'_>> {
let relpath = path.try_into()?;
let path = std::ffi::CString::new(relpath.as_str()).unwrap();
let mut locks = std::ptr::null_mut();
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_get_locks2(
self.ptr,
&mut locks,
path.as_ptr(),
depth.into(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let hash = unsafe { apr::hash::Hash::from_ptr(locks) };
Ok(hash
.iter()
.map(|(k, v)| {
let lock_pool = Pool::new();
let duplicated = unsafe {
subversion_sys::svn_lock_dup(
v as *const subversion_sys::svn_lock_t,
lock_pool.as_mut_ptr(),
)
};
let pool_handle = apr::PoolHandle::owned(lock_pool);
(
String::from_utf8_lossy(k).into_owned(),
crate::Lock::from_raw(duplicated, pool_handle),
)
})
.collect())
}
pub fn replay(
&mut self,
revision: Revnum,
low_water_mark: Revnum,
send_deltas: bool,
editor: &mut crate::delta::WrapEditor,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let (editor_ptr, editor_baton) = editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_ra_replay(
self.ptr,
revision.into(),
low_water_mark.into(),
send_deltas.into(),
editor_ptr,
editor_baton,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn replay_range(
&mut self,
start_revision: Revnum,
end_revision: Revnum,
low_water_mark: Revnum,
send_deltas: bool,
mut revstart: impl FnMut(
Revnum,
&HashMap<String, Vec<u8>>,
) -> Result<crate::delta::WrapEditor<'static>, Error<'static>>,
mut revfinish: impl FnMut(
Revnum,
&HashMap<String, Vec<u8>>,
&mut crate::delta::WrapEditor<'static>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
struct ReplayRangeBaton {
revstart: *mut std::ffi::c_void,
revfinish: *mut std::ffi::c_void,
current_editor: Option<crate::delta::WrapEditor<'static>>,
}
fn replay_range_inner<RS, RF>(
session_ptr: *mut subversion_sys::svn_ra_session_t,
start_revision: Revnum,
end_revision: Revnum,
low_water_mark: Revnum,
send_deltas: bool,
revstart: &mut RS,
revfinish: &mut RF,
) -> Result<(), Error<'static>>
where
RS: FnMut(
Revnum,
&HashMap<String, Vec<u8>>,
) -> Result<crate::delta::WrapEditor<'static>, Error<'static>>,
RF: FnMut(
Revnum,
&HashMap<String, Vec<u8>>,
&mut crate::delta::WrapEditor<'static>,
) -> Result<(), Error<'static>>,
{
extern "C" fn revstart_wrapper<RS>(
revision: subversion_sys::svn_revnum_t,
replay_baton: *mut std::ffi::c_void,
editor: *mut *const subversion_sys::svn_delta_editor_t,
edit_baton: *mut *mut std::ffi::c_void,
rev_props: *mut apr_sys::apr_hash_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t
where
RS: FnMut(
Revnum,
&HashMap<String, Vec<u8>>,
)
-> Result<crate::delta::WrapEditor<'static>, Error<'static>>,
{
let baton = unsafe { &mut *(replay_baton as *mut ReplayRangeBaton) };
let rev_props_map = if rev_props.is_null() {
HashMap::new()
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
prop_hash.to_hashmap()
};
let rev = match Revnum::from_raw(revision) {
Some(r) => r,
None => {
return unsafe {
Error::from_message("Invalid revision in replay_range revstart")
.into_raw()
};
}
};
let revstart = unsafe { &mut *(baton.revstart as *mut RS) };
match revstart(rev, &rev_props_map) {
Ok(wrap_editor) => {
baton.current_editor = Some(wrap_editor);
let (editor_ptr, editor_baton_ptr) =
baton.current_editor.as_ref().unwrap().as_raw_parts();
unsafe {
*editor = editor_ptr;
*edit_baton = editor_baton_ptr;
}
std::ptr::null_mut()
}
Err(e) => unsafe { e.into_raw() },
}
}
extern "C" fn revfinish_wrapper<RF>(
revision: subversion_sys::svn_revnum_t,
replay_baton: *mut std::ffi::c_void,
_editor: *const subversion_sys::svn_delta_editor_t,
_edit_baton: *mut std::ffi::c_void,
rev_props: *mut apr_sys::apr_hash_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t
where
RF: FnMut(
Revnum,
&HashMap<String, Vec<u8>>,
&mut crate::delta::WrapEditor<'static>,
) -> Result<(), Error<'static>>,
{
let baton = unsafe { &mut *(replay_baton as *mut ReplayRangeBaton) };
let rev_props_map = if rev_props.is_null() {
HashMap::new()
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
prop_hash.to_hashmap()
};
let rev = match Revnum::from_raw(revision) {
Some(r) => r,
None => {
return unsafe {
Error::from_message("Invalid revision in replay_range revfinish")
.into_raw()
};
}
};
let revfinish = unsafe { &mut *(baton.revfinish as *mut RF) };
let result = match baton.current_editor.as_mut() {
Some(editor) => revfinish(rev, &rev_props_map, editor),
None => Err(Error::from_message(
"revfinish called without a current editor",
)),
};
baton.current_editor = None;
match result {
Ok(()) => std::ptr::null_mut(),
Err(e) => unsafe { e.into_raw() },
}
}
let mut baton = ReplayRangeBaton {
revstart: revstart as *mut RS as *mut std::ffi::c_void,
revfinish: revfinish as *mut RF as *mut std::ffi::c_void,
current_editor: None,
};
let baton_ptr = &mut baton as *mut ReplayRangeBaton;
let pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_replay_range(
session_ptr,
start_revision.into(),
end_revision.into(),
low_water_mark.into(),
send_deltas.into(),
Some(revstart_wrapper::<RS>),
Some(revfinish_wrapper::<RF>),
baton_ptr as *mut std::ffi::c_void,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
replay_range_inner(
self.ptr,
start_revision,
end_revision,
low_water_mark,
send_deltas,
&mut revstart,
&mut revfinish,
)
}
pub fn get_file_revs(
&mut self,
path: impl TryInto<RelPath, Error = Error<'static>>,
start: Revnum,
end: Revnum,
include_merged_revisions: bool,
file_rev_handler: impl FnMut(
&str,
Revnum,
&HashMap<String, Vec<u8>>,
bool,
Option<(&str, Revnum)>,
Option<(&str, Revnum)>,
&HashMap<String, Vec<u8>>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let relpath = path.try_into()?;
let path_cstr = std::ffi::CString::new(relpath.as_str())?;
let pool = Pool::new();
let handler_box: Box<
dyn FnMut(
&str,
Revnum,
&HashMap<String, Vec<u8>>,
bool,
Option<(&str, Revnum)>,
Option<(&str, Revnum)>,
&HashMap<String, Vec<u8>>,
) -> Result<(), Error<'static>>,
> = Box::new(file_rev_handler);
let handler_ptr = Box::into_raw(Box::new(handler_box));
extern "C" fn file_rev_handler_wrapper(
baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
rev: subversion_sys::svn_revnum_t,
rev_props: *mut apr_sys::apr_hash_t,
result_of_merge: subversion_sys::svn_boolean_t,
txdelta_handler: *mut subversion_sys::svn_txdelta_window_handler_t,
txdelta_baton: *mut *mut std::ffi::c_void,
prop_diffs: *mut apr_sys::apr_array_header_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
type HandlerFn = dyn FnMut(
&str,
Revnum,
&HashMap<String, Vec<u8>>,
bool,
Option<(&str, Revnum)>,
Option<(&str, Revnum)>,
&HashMap<String, Vec<u8>>,
) -> Result<(), Error<'static>>;
let handler_box: &mut Box<HandlerFn> = unsafe { &mut *(baton as *mut Box<HandlerFn>) };
let handler: &mut HandlerFn = &mut **handler_box;
let path_str = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() };
let rev_props_hash = if rev_props.is_null() {
HashMap::new()
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
prop_hash.to_hashmap()
};
let prop_diffs_map = if prop_diffs.is_null() {
HashMap::new()
} else {
let array = unsafe {
std::slice::from_raw_parts(
(*prop_diffs).elts as *const subversion_sys::svn_prop_t,
(*prop_diffs).nelts as usize,
)
};
let mut props = HashMap::new();
for prop in array {
let name = unsafe { std::ffi::CStr::from_ptr(prop.name).to_str().unwrap() };
let value = if prop.value.is_null() {
Vec::new()
} else {
unsafe {
Vec::from(std::slice::from_raw_parts(
(*prop.value).data as *const u8,
(*prop.value).len,
))
}
};
props.insert(name.to_string(), value);
}
props
};
match handler(
path_str,
Revnum::from_raw(rev).unwrap(),
&rev_props_hash,
result_of_merge != 0,
None, None, &prop_diffs_map,
) {
Ok(()) => {
unsafe {
if !txdelta_handler.is_null() {
*txdelta_handler = None;
}
if !txdelta_baton.is_null() {
*txdelta_baton = std::ptr::null_mut();
}
}
std::ptr::null_mut()
}
Err(e) => unsafe { e.into_raw() },
}
}
let err = unsafe {
subversion_sys::svn_ra_get_file_revs2(
self.ptr,
path_cstr.as_ptr(),
start.into(),
end.into(),
include_merged_revisions.into(),
Some(file_rev_handler_wrapper),
handler_ptr as *mut std::ffi::c_void,
pool.as_mut_ptr(),
)
};
unsafe {
let _ = Box::from_raw(handler_ptr);
}
Error::from_raw(err)?;
Ok(())
}
pub fn get_inherited_props(
&mut self,
path: &str,
revision: Revnum,
) -> Result<Vec<(String, HashMap<String, Vec<u8>>)>, Error<'_>> {
let path_cstr = std::ffi::CString::new(path)?;
let pool = Pool::new();
let mut inherited_props_array: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
let scratch_pool = Pool::new();
let err = unsafe {
subversion_sys::svn_ra_get_inherited_props(
self.ptr,
&mut inherited_props_array,
path_cstr.as_ptr(),
revision.into(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if inherited_props_array.is_null() {
return Ok(Vec::new());
}
let array = unsafe {
std::slice::from_raw_parts(
(*inherited_props_array).elts
as *const *const subversion_sys::svn_prop_inherited_item_t,
(*inherited_props_array).nelts as usize,
)
};
let mut result = Vec::new();
for item_ptr in array.iter() {
if item_ptr.is_null() {
continue;
}
let item = unsafe { &**item_ptr };
if item.path_or_url.is_null() {
continue;
}
let path_or_url = unsafe {
std::ffi::CStr::from_ptr(item.path_or_url)
.to_string_lossy()
.to_string()
};
let props = if item.prop_hash.is_null() {
HashMap::new()
} else {
let hash = unsafe { apr::hash::Hash::from_ptr(item.prop_hash) };
let mut props = HashMap::new();
for (key, value) in hash.iter() {
if value.is_null() {
continue;
}
let svn_str_ptr = value as *const subversion_sys::svn_string_t;
let svn_str = unsafe { &*svn_str_ptr };
let data = crate::svn_string_helpers::to_vec(svn_str);
props.insert(String::from_utf8_lossy(key).into_owned(), data);
}
props
};
result.push((path_or_url, props));
}
Ok(result)
}
pub fn do_diff<'s>(
&'s mut self,
revision: Revnum,
diff_target: &str,
options: &mut DoDiffOptions<'s>,
) -> Result<Box<dyn Reporter + Send + 's>, Error<'s>> {
let pool = Pool::new();
let mut reporter: *const subversion_sys::svn_ra_reporter3_t = std::ptr::null();
let mut report_baton: *mut std::ffi::c_void = std::ptr::null_mut();
let diff_target_cstr = std::ffi::CString::new(diff_target).unwrap();
let versus_url = crate::uri::canonicalize_uri(options.versus_url)?;
let versus_url_cstr = std::ffi::CString::new(versus_url.as_str()).unwrap();
let (editor_ptr, editor_baton) = options.diff_editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_ra_do_diff3(
self.ptr,
&mut reporter,
&mut report_baton,
revision.0,
diff_target_cstr.as_ptr(),
options.depth.into(),
options.ignore_ancestry as i32,
options.text_deltas as i32,
versus_url_cstr.as_ptr(),
editor_ptr,
editor_baton,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(Box::new(WrapReporter {
reporter,
baton: report_baton,
_pool: pool,
callback_batons: Vec::new(),
_phantom: PhantomData,
}))
}
pub fn get_dir_props(
&mut self,
path: &str,
revision: Revnum,
) -> Result<(HashMap<String, Vec<u8>>, Revnum), Error<'_>> {
let pool = Pool::new();
let path_cstr = std::ffi::CString::new(path).unwrap();
let mut props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
let mut fetched_rev: subversion_sys::svn_revnum_t = 0;
let err = unsafe {
subversion_sys::svn_ra_get_dir2(
self.ptr,
std::ptr::null_mut(), &mut fetched_rev,
&mut props,
path_cstr.as_ptr(),
revision.0,
0, pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let properties = if props.is_null() {
HashMap::new()
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
prop_hash.to_hashmap()
};
Ok((properties, Revnum(fetched_rev as _)))
}
pub fn change_rev_prop2(
&mut self,
rev: Revnum,
name: &str,
old_value: Option<&[u8]>,
value: Option<&[u8]>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let name_cstr = std::ffi::CString::new(name).unwrap();
let old_value_svn = old_value.map(|v| crate::string::BStr::from_bytes(v, &pool));
let old_value_ptr = old_value_svn.as_ref().map(|v| v.as_ptr());
let old_value_ptr_ptr = match old_value_ptr {
Some(ptr) => &ptr as *const *const subversion_sys::svn_string_t,
None => std::ptr::null(),
};
let value_ptr = if let Some(val) = value {
let svn_str = crate::string::BStr::from_bytes(val, &pool);
svn_str.as_ptr()
} else {
std::ptr::null()
};
let err = unsafe {
subversion_sys::svn_ra_change_rev_prop2(
self.ptr,
rev.0,
name_cstr.as_ptr(),
old_value_ptr_ptr,
value_ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
}
pub fn modules() -> Result<String, Error<'static>> {
let pool = Pool::new();
let buf = unsafe {
subversion_sys::svn_stringbuf_create(
std::ffi::CStr::from_bytes_with_nul(b"\0").unwrap().as_ptr(),
pool.as_mut_ptr(),
)
};
let err = unsafe { subversion_sys::svn_ra_print_modules(buf, pool.as_mut_ptr()) };
Error::from_raw(err)?;
Ok(unsafe {
std::ffi::CStr::from_ptr((*buf).data)
.to_string_lossy()
.into_owned()
})
}
fn rust_reporter_vtable<R: Reporter>() -> subversion_sys::svn_ra_reporter3_t {
subversion_sys::svn_ra_reporter3_t {
set_path: Some(rust_reporter_set_path::<R>),
delete_path: Some(rust_reporter_delete_path::<R>),
link_path: Some(rust_reporter_link_path::<R>),
finish_report: Some(rust_reporter_finish_report::<R>),
abort_report: Some(rust_reporter_abort_report::<R>),
}
}
unsafe extern "C" fn rust_reporter_set_path<R: Reporter>(
report_baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
revision: subversion_sys::svn_revnum_t,
depth: subversion_sys::svn_depth_t,
start_empty: subversion_sys::svn_boolean_t,
lock_token: *const std::os::raw::c_char,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let reporter = &mut *(report_baton as *mut R);
let path_str = std::ffi::CStr::from_ptr(path).to_string_lossy();
let lock_token_str = if lock_token.is_null() {
""
} else {
std::ffi::CStr::from_ptr(lock_token).to_str().unwrap_or("")
};
let rev = Revnum::from_raw(revision).unwrap_or(Revnum::invalid());
match reporter.set_path(
&path_str,
rev,
Depth::from(depth),
start_empty != 0,
lock_token_str,
) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn rust_reporter_delete_path<R: Reporter>(
report_baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let reporter = &mut *(report_baton as *mut R);
let path_str = std::ffi::CStr::from_ptr(path).to_string_lossy();
match reporter.delete_path(&path_str) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn rust_reporter_link_path<R: Reporter>(
report_baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
url: *const std::os::raw::c_char,
revision: subversion_sys::svn_revnum_t,
depth: subversion_sys::svn_depth_t,
start_empty: subversion_sys::svn_boolean_t,
lock_token: *const std::os::raw::c_char,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let reporter = &mut *(report_baton as *mut R);
let path_str = std::ffi::CStr::from_ptr(path).to_string_lossy();
let url_str = std::ffi::CStr::from_ptr(url).to_string_lossy();
let lock_token_str = if lock_token.is_null() {
""
} else {
std::ffi::CStr::from_ptr(lock_token).to_str().unwrap_or("")
};
let rev = Revnum::from_raw(revision).unwrap_or(Revnum::invalid());
match reporter.link_path(
&path_str,
&url_str,
rev,
Depth::from(depth),
start_empty != 0,
lock_token_str,
) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn rust_reporter_finish_report<R: Reporter>(
report_baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let reporter = &mut *(report_baton as *mut R);
match reporter.finish_report() {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn rust_reporter_abort_report<R: Reporter>(
report_baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let reporter = &mut *(report_baton as *mut R);
match reporter.abort_report() {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
pub struct WrapReporter {
reporter: *const subversion_sys::svn_ra_reporter3_t,
baton: *mut std::ffi::c_void,
_pool: apr::Pool<'static>,
callback_batons: Vec<(*mut std::ffi::c_void, crate::delta::DropperFn)>,
_phantom: PhantomData<*mut ()>,
}
impl WrapReporter {
pub fn as_ptr(&self) -> *const subversion_sys::svn_ra_reporter3_t {
self.reporter
}
pub fn as_baton(&self) -> *mut std::ffi::c_void {
self.baton
}
pub fn from_rust_reporter<R: Reporter + 'static>(reporter: R) -> Self {
let pool = Pool::new();
let reporter_box = Box::new(reporter);
let reporter_ptr = Box::into_raw(reporter_box) as *mut std::ffi::c_void;
let dropper: crate::delta::DropperFn = |ptr| unsafe {
let _ = Box::from_raw(ptr as *mut R);
};
let vtable = rust_reporter_vtable::<R>();
let vtable_ptr = unsafe {
let ptr = apr_sys::apr_palloc(
pool.as_mut_ptr(),
std::mem::size_of::<subversion_sys::svn_ra_reporter3_t>(),
) as *mut subversion_sys::svn_ra_reporter3_t;
*ptr = vtable;
ptr as *const subversion_sys::svn_ra_reporter3_t
};
WrapReporter {
reporter: vtable_ptr,
baton: reporter_ptr,
_pool: pool,
callback_batons: vec![(reporter_ptr, dropper)],
_phantom: PhantomData,
}
}
}
impl Drop for WrapReporter {
fn drop(&mut self) {
for (ptr, dropper) in &self.callback_batons {
unsafe { dropper(*ptr) };
}
}
}
unsafe impl Send for WrapReporter {}
impl Reporter for WrapReporter {
fn set_path(
&mut self,
path: &str,
rev: Revnum,
depth: Depth,
start_empty: bool,
lock_token: &str,
) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path).unwrap();
let lock_token = std::ffi::CString::new(lock_token).unwrap();
let pool = Pool::new();
let err = unsafe {
(*self.reporter).set_path.unwrap()(
self.baton,
path.as_ptr(),
rev.into(),
depth.into(),
start_empty.into(),
lock_token.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
fn delete_path(&mut self, path: &str) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path).unwrap();
let pool = Pool::new();
let err = unsafe {
(*self.reporter).delete_path.unwrap()(self.baton, path.as_ptr(), pool.as_mut_ptr())
};
Error::from_raw(err)?;
Ok(())
}
fn link_path(
&mut self,
path: &str,
url: &str,
rev: Revnum,
depth: Depth,
start_empty: bool,
lock_token: &str,
) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path).unwrap();
let url = std::ffi::CString::new(url).unwrap();
let lock_token = std::ffi::CString::new(lock_token).unwrap();
let pool = Pool::new();
let err = unsafe {
(*self.reporter).link_path.unwrap()(
self.baton,
path.as_ptr(),
url.as_ptr(),
rev.into(),
depth.into(),
start_empty.into(),
lock_token.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
fn finish_report(&mut self) -> Result<(), Error<'static>> {
let pool = Pool::new();
let err = unsafe { (*self.reporter).finish_report.unwrap()(self.baton, pool.as_mut_ptr()) };
Error::from_raw(err)?;
Ok(())
}
fn abort_report(&mut self) -> Result<(), Error<'static>> {
let pool = Pool::new();
let err = unsafe { (*self.reporter).abort_report.unwrap()(self.baton, pool.as_mut_ptr()) };
Error::from_raw(err)?;
Ok(())
}
}
pub fn version() -> crate::Version {
unsafe { crate::Version(subversion_sys::svn_ra_version()) }
}
pub trait Reporter {
fn set_path(
&mut self,
path: &str,
rev: Revnum,
depth: Depth,
start_empty: bool,
lock_token: &str,
) -> Result<(), Error<'static>>;
fn delete_path(&mut self, path: &str) -> Result<(), Error<'static>>;
fn link_path(
&mut self,
path: &str,
url: &str,
rev: Revnum,
depth: Depth,
start_empty: bool,
lock_token: &str,
) -> Result<(), Error<'static>>;
fn finish_report(&mut self) -> Result<(), Error<'static>>;
fn abort_report(&mut self) -> Result<(), Error<'static>>;
}
pub struct Callbacks {
ptr: *mut subversion_sys::svn_ra_callbacks2_t,
_pool: apr::Pool<'static>,
auth_baton: Option<std::pin::Pin<Box<crate::auth::AuthBaton>>>,
cancel_func: Option<Box<Box<dyn Fn() -> Result<(), Error<'static>>>>>,
progress_func: Option<Box<Box<dyn Fn(i64, i64)>>>,
_phantom: PhantomData<*mut ()>,
}
impl Drop for Callbacks {
fn drop(&mut self) {
}
}
impl Default for Callbacks {
fn default() -> Self {
Self::new().unwrap()
}
}
impl Callbacks {
pub fn new() -> Result<Callbacks, crate::Error<'static>> {
let pool = apr::Pool::new();
let mut callbacks = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_ra_create_callbacks(&mut callbacks, pool.as_mut_ptr());
svn_result(err)?;
}
Ok(Callbacks {
ptr: callbacks,
_pool: pool,
auth_baton: None,
cancel_func: None,
progress_func: None,
_phantom: PhantomData,
})
}
pub fn set_auth_baton(&mut self, auth_baton: crate::auth::AuthBaton) {
let mut pinned_baton = Box::pin(auth_baton);
unsafe {
let baton_ptr = pinned_baton.as_mut().get_mut().as_mut_ptr();
(*self.ptr).auth_baton = baton_ptr;
}
self.auth_baton = Some(pinned_baton);
}
pub unsafe fn set_auth_baton_borrowed(&mut self, auth_baton: &mut crate::auth::AuthBaton) {
(*self.ptr).auth_baton = auth_baton.as_mut_ptr();
}
pub fn set_cancel_func(
&mut self,
cancel_func: impl Fn() -> Result<(), Error<'static>> + 'static,
) {
let boxed: Box<Box<dyn Fn() -> Result<(), Error<'static>>>> =
Box::new(Box::new(cancel_func));
unsafe {
(*self.ptr).cancel_func = Some(crate::wrap_cancel_func);
}
self.cancel_func = Some(boxed);
}
pub fn set_progress_func(&mut self, progress_func: impl Fn(i64, i64) + 'static) {
let boxed: Box<Box<dyn Fn(i64, i64)>> = Box::new(Box::new(progress_func));
let baton_ptr = Box::into_raw(boxed) as *mut std::ffi::c_void;
unsafe {
(*self.ptr).progress_func = Some(wrap_progress_func);
(*self.ptr).progress_baton = baton_ptr;
}
self.progress_func =
Some(unsafe { Box::from_raw(baton_ptr as *mut Box<dyn Fn(i64, i64)>) });
}
fn get_callback_baton(&self) -> *mut std::ffi::c_void {
self.cancel_func
.as_ref()
.map(|b| {
b.as_ref() as *const Box<dyn Fn() -> Result<(), Error<'static>>>
as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut())
}
fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_ra_callbacks2_t {
self.ptr
}
}
extern "C" fn wrap_progress_func(
progress: i64,
total: i64,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) {
if baton.is_null() {
return;
}
let callback = unsafe { &*(baton as *const Box<dyn Fn(i64, i64)>) };
callback(progress, total);
}
pub fn api_version() -> (i32, i32, i32) {
(
subversion_sys::SVN_VER_MAJOR as i32,
subversion_sys::SVN_VER_MINOR as i32,
subversion_sys::SVN_VER_PATCH as i32,
)
}
pub fn abi_version() -> i32 {
subversion_sys::SVN_RA_ABI_VERSION as i32
}
#[cfg(all(test, feature = "repos"))]
mod tests {
use super::*;
use crate::mergeinfo::MergeinfoInheritance;
use crate::{LocationSegment, Lock};
fn create_test_repo() -> (tempfile::TempDir, String, crate::repos::Repos) {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = crate::repos::Repos::create(&repo_path).unwrap();
let url = crate::path_to_file_url(&repo_path);
(temp_dir, url, repo)
}
fn create_test_repo_with_session() -> (
tempfile::TempDir,
crate::repos::Repos,
Session<'static>,
Box<Callbacks>,
) {
let (temp_dir, url, repo) = create_test_repo();
let mut callbacks = Box::new(Callbacks::new().unwrap());
let providers = vec![
crate::auth::get_username_provider(),
crate::auth::get_simple_provider(None::<&fn(&str) -> Result<bool, Error<'static>>>),
];
let mut auth_baton = crate::auth::AuthBaton::open(providers).unwrap();
auth_baton
.set(crate::auth::AuthSetting::DefaultUsername("testuser"))
.unwrap();
callbacks.set_auth_baton(auth_baton);
let (session, _, _) = unsafe {
let callbacks_ptr = &mut *callbacks as *mut Callbacks;
Session::open(&url, None, Some(&mut *callbacks_ptr), None).unwrap()
};
(temp_dir, repo, session, callbacks)
}
#[test]
fn test_callbacks_creation() {
let callbacks = Callbacks::new().unwrap();
assert!(!callbacks.ptr.is_null());
}
#[test]
fn test_set_auth_baton_borrowed() {
let (_temp_dir, url, _repo) = create_test_repo();
let providers = vec![crate::auth::get_username_provider()];
let mut auth_baton = crate::auth::AuthBaton::open(providers).unwrap();
auth_baton
.set(crate::auth::AuthSetting::DefaultUsername("testuser"))
.unwrap();
let mut callbacks = Box::new(Callbacks::new().unwrap());
unsafe { callbacks.set_auth_baton_borrowed(&mut auth_baton) };
let callbacks_ptr = &mut *callbacks as *mut Callbacks;
let (mut session, _, _) =
unsafe { Session::open(&url, None, Some(&mut *callbacks_ptr), None).unwrap() };
let latest = session.get_latest_revnum().unwrap();
assert_eq!(latest, Revnum(0));
}
#[test]
fn test_get_repository_info() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let info = session.get_repository_info().unwrap();
assert!(!info.uuid.is_empty());
assert!(info.root_url.starts_with("file://"));
assert_eq!(info.latest_revision, crate::Revnum(0)); assert!(info.session_url.starts_with("file://"));
assert_eq!(info.root_url, info.session_url); }
#[test]
fn test_path_exists() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
assert!(session.path_exists("", crate::Revnum(0)).unwrap());
assert!(!session
.path_exists("nonexistent.txt", crate::Revnum(0))
.unwrap());
}
#[test]
fn test_get_files() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/file1.txt").unwrap();
let mut stream1 = txn_root.apply_text("/file1.txt", None).unwrap();
stream1.write(b"Content of file 1").unwrap();
stream1.close().unwrap();
txn_root.make_file("/file2.txt").unwrap();
let mut stream2 = txn_root.apply_text("/file2.txt", None).unwrap();
stream2.write(b"Content of file 2").unwrap();
stream2.close().unwrap();
txn.commit().unwrap();
let files = session
.get_files(&["file1.txt", "file2.txt"], crate::Revnum(1))
.unwrap();
assert_eq!(files.len(), 2);
assert_eq!(files[0].0, "file1.txt");
assert_eq!(files[0].1, b"Content of file 1");
assert_eq!(files[1].0, "file2.txt");
assert_eq!(files[1].1, b"Content of file 2");
}
#[test]
fn test_get_file_with_wrap_write() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/testfile.txt").unwrap();
let mut stream = txn_root.apply_text("/testfile.txt", None).unwrap();
stream.write(b"Test content from file").unwrap();
stream.close().unwrap();
let rev = txn.commit().unwrap();
println!("Committed revision: {}", rev.0);
let mut buffer: Vec<u8> = Vec::new();
let mut output_stream = crate::io::wrap_write(&mut buffer).unwrap();
output_stream.write(b"Direct write test").unwrap();
output_stream.close().unwrap();
println!(
"Buffer after direct write: {:?}",
String::from_utf8_lossy(&buffer)
);
assert_eq!(buffer, b"Direct write test");
buffer.clear();
let mut output_stream = crate::io::wrap_write(&mut buffer).unwrap();
println!("Calling get_file...");
let (fetched_rev, props) = session
.get_file("testfile.txt", crate::Revnum(1), &mut output_stream)
.unwrap();
println!(
"get_file returned: fetched_rev={:?}, props.len()={}",
fetched_rev,
props.len()
);
output_stream.close().unwrap();
println!(
"Buffer after get_file: {:?}",
String::from_utf8_lossy(&buffer)
);
assert_eq!(
fetched_rev, None,
"fetched_rev should be None when specific revision is passed"
);
assert_eq!(
buffer, b"Test content from file",
"Stream should contain the file contents"
);
assert!(!props.is_empty(), "Properties should be returned");
}
#[test]
fn test_get_file_with_invalid_revnum() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/testfile.txt").unwrap();
let mut stream = txn_root.apply_text("/testfile.txt", None).unwrap();
stream.write(b"Test content").unwrap();
stream.close().unwrap();
let committed_rev = txn.commit().unwrap();
let mut buffer: Vec<u8> = Vec::new();
let mut output_stream = crate::io::wrap_write(&mut buffer).unwrap();
let (fetched_rev, props) = session
.get_file("testfile.txt", crate::Revnum(-1), &mut output_stream)
.unwrap();
output_stream.close().unwrap();
assert_eq!(
fetched_rev,
Some(committed_rev),
"fetched_rev should be Some({}) when SVN_INVALID_REVNUM is passed",
committed_rev.0
);
assert_eq!(buffer, b"Test content");
assert!(!props.is_empty());
}
#[test]
fn test_repo_creation_only() {
let (_temp_dir, url, _repo) = create_test_repo();
println!("Created repo at: {}", url);
}
#[test]
fn test_session_opening_only() {
let (_temp_dir, url, _repo) = create_test_repo();
let result = Session::open(&url, None, None, None);
println!("Session open result: {:?}", result.is_ok());
}
#[test]
fn test_simple_get_log() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let mut call_count = 0;
let mut log_receiver = |_log_entry: &crate::LogEntry| -> Result<(), Error<'static>> {
call_count += 1;
Ok(())
};
let result = session.get_log(
&[""],
crate::Revnum::from(0u32),
crate::Revnum::from(0u32),
&GetLogOptions::default(),
&mut log_receiver,
);
println!(
"get_log result: {:?}, calls: {}",
result.is_ok(),
call_count
);
}
#[test]
fn test_get_log_with_error() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let mut log_receiver = |_log_entry: &crate::LogEntry| -> Result<(), Error<'static>> {
Err(Error::from_message("Test error from callback"))
};
let result = session.get_log(
&[""],
crate::Revnum::from(0u32),
crate::Revnum::from(0u32),
&GetLogOptions::default(),
&mut log_receiver,
);
println!("get_log with error result: {:?}", result);
assert!(result.is_err());
}
#[test]
fn test_session_url_validation() {
let result = Session::open("not-a-url", None, None, None);
assert!(result.is_err());
let _result = Session::open("file:///tmp/test", None, None, None);
}
#[test]
fn test_reporter_trait_safety() {
fn _assert_send<T: Send>() {}
_assert_send::<WrapReporter>();
}
#[test]
fn test_lock_struct() {
let pool = apr::Pool::new();
let lock_raw: *mut subversion_sys::svn_lock_t = pool.calloc();
unsafe {
(*lock_raw).path = apr::strings::pstrdup_raw("/test/path", &pool).unwrap() as *const _;
(*lock_raw).token = apr::strings::pstrdup_raw("lock-token", &pool).unwrap() as *const _;
(*lock_raw).owner = apr::strings::pstrdup_raw("test-owner", &pool).unwrap() as *const _;
(*lock_raw).comment =
apr::strings::pstrdup_raw("test comment", &pool).unwrap() as *const _;
(*lock_raw).is_dav_comment = 0;
(*lock_raw).creation_date = 0;
(*lock_raw).expiration_date = 0;
}
let pool_handle = apr::PoolHandle::owned(pool);
let lock = Lock::from_raw(lock_raw, pool_handle);
assert_eq!(lock.path(), "/test/path");
assert_eq!(lock.token(), "lock-token");
assert_eq!(lock.owner(), "test-owner");
assert_eq!(lock.comment(), "test comment");
assert!(!lock.is_dav_comment());
}
#[test]
fn test_dirent_struct() {
let pool = apr::Pool::new();
let dirent_raw: *mut subversion_sys::svn_dirent_t = pool.calloc();
unsafe {
(*dirent_raw).kind = subversion_sys::svn_node_kind_t_svn_node_file;
(*dirent_raw).size = 1024;
(*dirent_raw).has_props = 1;
(*dirent_raw).created_rev = 42;
(*dirent_raw).time = 1000000;
(*dirent_raw).last_author =
apr::strings::pstrdup_raw("author", &pool).unwrap() as *const _;
}
let dirent = Dirent::from_raw(dirent_raw);
assert_eq!(dirent.kind(), crate::NodeKind::File);
assert_eq!(dirent.size(), 1024);
assert!(dirent.has_props());
assert_eq!(dirent.created_rev(), Some(crate::Revnum(42)));
assert_eq!(dirent.last_author(), Some("author"));
}
#[test]
fn test_location_segment() {
let pool = apr::Pool::new();
let segment_raw: *mut subversion_sys::svn_location_segment_t = pool.calloc();
unsafe {
(*segment_raw).range_start = 10;
(*segment_raw).range_end = 20;
(*segment_raw).path =
apr::strings::pstrdup_raw("/trunk/src", &pool).unwrap() as *const _;
}
let segment = LocationSegment::from_raw(segment_raw);
let range = segment.range();
assert_eq!(range.start, crate::Revnum(20)); assert_eq!(range.end, crate::Revnum(10));
assert_eq!(segment.path(), "/trunk/src");
}
#[test]
fn test_mergeinfo_inheritance() {
assert_eq!(
MergeinfoInheritance::from(
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited
),
MergeinfoInheritance::Inherited
);
assert_eq!(
MergeinfoInheritance::from(
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor
),
MergeinfoInheritance::NearestAncestor
);
assert_eq!(
MergeinfoInheritance::from(
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit
),
MergeinfoInheritance::Explicit
);
}
#[test]
fn test_file_revision_struct() {
let pool = apr::Pool::new();
let fr_raw: *mut subversion_sys::svn_repos_node_t = pool.calloc();
unsafe {
(*fr_raw).copyfrom_path = std::ptr::null();
(*fr_raw).copyfrom_rev = -1; (*fr_raw).action = b'A' as i8;
(*fr_raw).text_mod = 1;
(*fr_raw).prop_mod = 1;
(*fr_raw).kind = subversion_sys::svn_node_kind_t_svn_node_file;
}
}
#[test]
fn test_no_send_no_sync() {
fn assert_not_send<T>()
where
T: ?Sized,
{
}
fn assert_not_sync<T>()
where
T: ?Sized,
{
}
assert_not_send::<Session>();
assert_not_sync::<Session>();
}
#[test]
fn test_get_file_revs() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let mut handler_called = false;
let result = session.get_file_revs(
"nonexistent.txt",
crate::Revnum(1),
crate::Revnum(1),
false,
|_path, _rev, _rev_props, _result_of_merge, _copyfrom, _merged, _prop_diffs| {
handler_called = true;
Ok(())
},
);
assert!(result.is_err());
assert!(!handler_called);
}
#[test]
fn test_get_file_revs_collects_revisions() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Initial content\n").unwrap();
}
root.change_node_prop("/test.txt", "custom:prop1", b"value1")
.unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(1u32), 0).unwrap();
let mut root = txn.root().unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Modified content\n").unwrap();
}
root.change_node_prop("/test.txt", "custom:prop2", b"value2")
.unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(2u32), 0).unwrap();
let mut root = txn.root().unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Final content\n").unwrap();
}
txn.commit().unwrap();
let mut revisions_seen = Vec::new();
let result = session.get_file_revs(
"test.txt",
crate::Revnum(1),
crate::Revnum(3),
false,
|_path, rev, _rev_props, _result_of_merge, _copyfrom, _merged, prop_diffs| {
revisions_seen.push(rev.0);
let _ = prop_diffs.len(); Ok(())
},
);
assert!(result.is_ok(), "get_file_revs should succeed");
assert!(
!revisions_seen.is_empty(),
"Should have seen at least one revision"
);
assert!(revisions_seen.contains(&1), "Should have seen revision 1");
}
#[test]
fn test_get_file_revs_empty_range() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Content\n").unwrap();
}
txn.commit().unwrap();
let mut handler_called = false;
let result = session.get_file_revs(
"test.txt",
crate::Revnum(10),
crate::Revnum(1),
false,
|_path, _rev, _rev_props, _result_of_merge, _copyfrom, _merged, _prop_diffs| {
handler_called = true;
Ok(())
},
);
if result.is_ok() {
assert!(
!handler_called,
"Handler should not be called for invalid range"
);
}
}
#[test]
fn test_get_inherited_props() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_dir("/trunk/src").unwrap();
root.make_dir("/trunk/src/lib").unwrap();
root.change_node_prop("/trunk", "prop:level1", b"value1")
.unwrap();
root.change_node_prop("/trunk/src", "prop:level2", b"value2")
.unwrap();
root.change_node_prop("/trunk/src/lib", "prop:level3", b"value3")
.unwrap();
let rev = txn.commit().unwrap();
let inherited = session.get_inherited_props("trunk/src/lib", rev).unwrap();
for (path, props) in &inherited {
println!("Inherited from {}: {:?}", path, props);
if path.ends_with("trunk") {
assert!(props.contains_key("prop:level1"));
} else if path.ends_with("trunk/src") {
assert!(props.contains_key("prop:level2"));
}
}
}
#[test]
fn test_get_log_full() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut revisions = Vec::new();
for i in 1..=3 {
let base_rev = if revisions.is_empty() {
crate::Revnum::from(0u32)
} else {
*revisions.last().unwrap()
};
let mut txn = fs.begin_txn(base_rev, 0).unwrap();
let mut root = txn.root().unwrap();
let filename = format!("/file{}.txt", i);
root.make_file(filename.as_str()).unwrap();
let mut stream = root.apply_text(filename.as_str(), None).unwrap();
use std::io::Write;
writeln!(stream, "Content for file {}", i).unwrap();
drop(stream);
txn.change_prop("svn:log", &format!("Commit {}", i))
.unwrap();
txn.change_prop("svn:author", "test-user").unwrap();
let rev = txn.commit().unwrap();
revisions.push(rev);
}
let mut log_entries: Vec<(crate::Revnum, String, String)> = Vec::new();
session
.get_log(
&[""], revisions[0],
*revisions.last().unwrap(),
&GetLogOptions::default()
.with_discover_changed_paths(true)
.with_revprops(Some(&["svn:log", "svn:author", "svn:date"])),
&mut |log_entry| {
if let Some(revision) = log_entry.revision() {
let author = log_entry.author().unwrap_or("").to_string();
let message = log_entry.message().unwrap_or("").to_string();
log_entries.push((revision, author, message));
}
Ok(())
},
)
.unwrap();
assert_eq!(log_entries.len(), 3);
for (i, (rev, author, message)) in log_entries.iter().enumerate() {
assert_eq!(author, "test-user");
assert_eq!(message, &format!("Commit {}", i + 1));
assert!(rev.as_u64() >= 1);
}
}
#[test]
fn test_iter_logs() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut revisions = Vec::new();
for i in 1..=3 {
let base_rev = if revisions.is_empty() {
crate::Revnum::from(0u32)
} else {
*revisions.last().unwrap()
};
let mut txn = fs.begin_txn(base_rev, 0).unwrap();
let mut root = txn.root().unwrap();
let filename = format!("/file{}.txt", i);
root.make_file(filename.as_str()).unwrap();
let mut stream = root.apply_text(filename.as_str(), None).unwrap();
use std::io::Write;
writeln!(stream, "Content for file {}", i).unwrap();
drop(stream);
txn.change_prop("svn:log", &format!("Commit {}", i))
.unwrap();
txn.change_prop("svn:author", "test-user").unwrap();
let rev = txn.commit().unwrap();
revisions.push(rev);
}
let entries: Vec<crate::Revnum> = session
.iter_logs(
&[""],
revisions[0],
*revisions.last().unwrap(),
&GetLogOptions::default()
.with_discover_changed_paths(true)
.with_revprops(Some(&["svn:log", "svn:author"])),
)
.map(|e| e.unwrap().revision().unwrap())
.collect();
assert_eq!(entries.len(), 3);
}
#[test]
fn test_iter_logs_early_drop() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
for i in 1..=5 {
let base_rev = crate::Revnum::from((i - 1) as u32);
let mut txn = fs.begin_txn(base_rev, 0).unwrap();
let mut root = txn.root().unwrap();
let filename = format!("/file{}.txt", i);
root.make_file(filename.as_str()).unwrap();
let mut stream = root.apply_text(filename.as_str(), None).unwrap();
use std::io::Write;
writeln!(stream, "Content {}", i).unwrap();
drop(stream);
txn.change_prop("svn:log", &format!("Commit {}", i))
.unwrap();
let _rev = txn.commit().unwrap();
}
{
let mut iter = session.iter_logs(
&[""],
crate::Revnum::from(1u32),
crate::Revnum::from(5u32),
&GetLogOptions::default(),
);
let first = iter.next().unwrap().unwrap();
assert!(first.revision().is_some());
let second = iter.next().unwrap().unwrap();
assert!(second.revision().is_some());
}
let latest = session.get_latest_revnum().unwrap();
assert_eq!(latest, crate::Revnum::from(5u32));
}
#[test]
fn test_iter_logs_error() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let mut iter = session.iter_logs(
&[""],
crate::Revnum::from(0u32),
crate::Revnum::from(1000u32),
&GetLogOptions::default(),
);
let result = iter.next();
assert!(
result.is_some(),
"iter_logs should yield an error, not be empty"
);
match result.unwrap() {
Ok(_) => panic!("expected an error, got Ok"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("1000"),
"error should mention the invalid revision: {}",
msg
);
}
}
}
#[test]
fn test_replay() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Hello\n").unwrap();
}
txn.commit().unwrap();
let pool = apr::Pool::new();
let mut editor = crate::delta::default_editor(pool);
let result = session.replay(crate::Revnum(1), crate::Revnum(0), true, &mut editor);
assert!(result.is_ok(), "replay should succeed: {:?}", result.err());
}
#[test]
fn test_replay_range() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Rev 1\n").unwrap();
}
txn.commit().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(1u32), 0).unwrap();
let mut root = txn.root().unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Rev 2\n").unwrap();
}
txn.commit().unwrap();
let mut revisions_started = Vec::new();
let mut revisions_finished = Vec::new();
let result = session.replay_range(
crate::Revnum(1),
crate::Revnum(2),
crate::Revnum(0),
true,
|rev, _rev_props| {
revisions_started.push(rev.0);
let pool = apr::Pool::new();
Ok(crate::delta::default_editor(pool))
},
|rev, _rev_props, _editor| {
revisions_finished.push(rev.0);
Ok(())
},
);
assert!(
result.is_ok(),
"replay_range should succeed: {:?}",
result.err()
);
assert_eq!(revisions_started, vec![1, 2]);
assert_eq!(revisions_finished, vec![1, 2]);
}
#[test]
fn test_replay_range_receives_rev_props() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
{
let mut stream = root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"content\n").unwrap();
}
txn.commit().unwrap();
let mut start_props = Vec::new();
let mut finish_props = Vec::new();
session
.replay_range(
crate::Revnum(1),
crate::Revnum(1),
crate::Revnum(0),
false,
|_rev, rev_props| {
start_props.push(rev_props.clone());
let pool = apr::Pool::new();
Ok(crate::delta::default_editor(pool))
},
|_rev, rev_props, _editor| {
finish_props.push(rev_props.clone());
Ok(())
},
)
.unwrap();
assert_eq!(start_props.len(), 1);
assert_eq!(finish_props.len(), 1);
assert!(
start_props[0].contains_key("svn:date"),
"rev_props should contain svn:date, got keys: {:?}",
start_props[0].keys().collect::<Vec<_>>()
);
}
#[test]
fn test_replay_with_rust_editor() {
use std::cell::RefCell;
use std::rc::Rc;
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/hello.txt").unwrap();
{
let mut stream = root.apply_text("/hello.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"hello\n").unwrap();
}
txn.commit().unwrap();
let ops = Rc::new(RefCell::new(Vec::<String>::new()));
struct RecordingEditor {
ops: Rc<RefCell<Vec<String>>>,
}
struct RecordingDir {
ops: Rc<RefCell<Vec<String>>>,
}
struct RecordingFile {
ops: Rc<RefCell<Vec<String>>>,
}
impl crate::delta::Editor for RecordingEditor {
type RootEditor = RecordingDir;
fn set_target_revision(
&mut self,
revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
self.ops
.borrow_mut()
.push(format!("set_target_revision({:?})", revision));
Ok(())
}
fn open_root(
&mut self,
base_revision: Option<crate::Revnum>,
) -> Result<RecordingDir, crate::Error<'_>> {
self.ops
.borrow_mut()
.push(format!("open_root({:?})", base_revision));
Ok(RecordingDir {
ops: self.ops.clone(),
})
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
self.ops.borrow_mut().push("close_edit".to_string());
Ok(())
}
fn abort(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
}
impl crate::delta::DirectoryEditor for RecordingDir {
type SubDirectory = RecordingDir;
type File = RecordingFile;
fn delete_entry(
&mut self,
_path: &str,
_rev: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn add_directory(
&mut self,
path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<RecordingDir, crate::Error<'_>> {
self.ops
.borrow_mut()
.push(format!("add_directory({})", path));
Ok(RecordingDir {
ops: self.ops.clone(),
})
}
fn open_directory(
&mut self,
path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<RecordingDir, crate::Error<'_>> {
self.ops
.borrow_mut()
.push(format!("open_directory({})", path));
Ok(RecordingDir {
ops: self.ops.clone(),
})
}
fn change_prop(
&mut self,
_name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn absent_directory(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn add_file(
&mut self,
path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<RecordingFile, crate::Error<'_>> {
self.ops.borrow_mut().push(format!("add_file({})", path));
Ok(RecordingFile {
ops: self.ops.clone(),
})
}
fn open_file(
&mut self,
path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<RecordingFile, crate::Error<'_>> {
self.ops.borrow_mut().push(format!("open_file({})", path));
Ok(RecordingFile {
ops: self.ops.clone(),
})
}
fn absent_file(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
Ok(())
}
}
impl crate::delta::FileEditor for RecordingFile {
fn apply_textdelta(
&mut self,
_base_checksum: Option<&str>,
) -> Result<
Box<
dyn for<'a> Fn(
&'a mut crate::delta::TxDeltaWindow,
) -> Result<(), crate::Error<'static>>,
>,
crate::Error<'static>,
> {
self.ops.borrow_mut().push("apply_textdelta".to_string());
Ok(Box::new(|_| Ok(())))
}
fn change_prop(
&mut self,
_name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'static>> {
Ok(())
}
fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
Ok(())
}
}
let editor = RecordingEditor { ops: ops.clone() };
let mut wrap = crate::delta::WrapEditor::from_rust_editor(editor);
session
.replay(crate::Revnum(1), crate::Revnum(0), true, &mut wrap)
.unwrap();
let recorded = ops.borrow();
assert!(
recorded.iter().any(|s| s.starts_with("open_root")),
"replay should have called open_root, got: {:?}",
*recorded
);
assert!(
recorded.iter().any(|s| s.contains("hello.txt")),
"replay should have referenced hello.txt, got: {:?}",
*recorded
);
}
#[test]
fn test_lock_unlock() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/lockable.txt").unwrap();
let mut stream = root.apply_text("/lockable.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Content to lock\n").unwrap();
drop(stream);
let rev = txn.commit().unwrap();
let mut lock_paths = HashMap::new();
lock_paths.insert("lockable.txt".to_string(), rev);
let lock_called = std::rc::Rc::new(std::cell::RefCell::new(false));
let received_token = std::rc::Rc::new(std::cell::RefCell::new(String::new()));
let lock_called_clone = lock_called.clone();
let received_token_clone = received_token.clone();
session
.lock(
&lock_paths,
"Test lock comment",
false, move |path, locked, lock, error| {
*lock_called_clone.borrow_mut() = true;
if locked {
let lock = lock.expect("lock callback should provide lock when locked");
println!("Locked path: {} with token: {}", path, lock.token());
*received_token_clone.borrow_mut() = lock.token().to_string();
} else if let Some(err) = error {
println!("Failed to lock {}: {:?}", path, err.message());
}
Ok(())
},
)
.unwrap();
assert!(
*lock_called.borrow(),
"Lock callback should have been called"
);
assert!(
!received_token.borrow().is_empty(),
"Should have received a lock token"
);
let lock = session
.get_lock("lockable.txt")
.unwrap()
.expect("Lock should exist");
assert_eq!(lock.path(), "/lockable.txt"); assert!(!lock.token().is_empty());
let mut unlock_paths = HashMap::new();
unlock_paths.insert("lockable.txt".to_string(), lock.token().to_string());
session
.unlock(
&unlock_paths,
false, |path, unlocked, _lock, error| {
if unlocked {
println!("Unlocked path: {}", path);
} else if let Some(err) = error {
println!("Failed to unlock {}: {:?}", path, err.message());
}
Ok(())
},
)
.unwrap();
}
#[test]
fn test_get_locations() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/original.txt").unwrap();
let mut stream = root.apply_text("/original.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Original content\n").unwrap();
drop(stream);
let rev1 = txn.commit().unwrap();
let mut txn = fs.begin_txn(rev1, 0).unwrap();
let mut root = txn.root().unwrap();
let mut stream = root.apply_text("/original.txt", None).unwrap();
stream.write_all(b"Modified content\n").unwrap();
drop(stream);
let rev2 = txn.commit().unwrap();
let locations = session
.get_locations(
"original.txt",
rev2, &[rev1, rev2], )
.unwrap();
assert!(locations.contains_key(&rev1));
assert!(locations.contains_key(&rev2));
assert_eq!(locations.get(&rev1).unwrap(), "/original.txt");
assert_eq!(locations.get(&rev2).unwrap(), "/original.txt");
}
#[test]
fn test_get_dir_props() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let (props, fetched_rev) = session.get_dir_props("", crate::Revnum(0)).unwrap();
assert_eq!(fetched_rev.0, 0);
assert!(props.is_empty() || !props.is_empty());
}
#[test]
fn test_do_diff() {
println!("do_diff function with options struct exists and has correct signature");
}
#[test]
fn test_change_rev_prop2() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let result = session.change_rev_prop2(
crate::Revnum(0),
"test:property",
None, Some(b"new value"),
);
match result {
Ok(()) => {
let prop = session.rev_prop(crate::Revnum(0), "test:property").unwrap();
assert_eq!(prop.unwrap(), b"new value");
}
Err(_) => {
}
}
}
#[test]
fn test_reparent_and_session_url() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let initial_url = session.get_session_url().unwrap();
assert!(initial_url.starts_with("file://"));
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/subdir").unwrap();
let _rev = txn.commit().unwrap();
let new_url = format!("{}/subdir", initial_url);
let result = session.reparent(&new_url);
assert!(result.is_ok(), "Reparent should succeed: {:?}", result);
let current_url = session.get_session_url().unwrap();
assert_eq!(current_url, new_url);
assert!(current_url.ends_with("/subdir"));
let result = session.reparent(&initial_url);
assert!(result.is_ok(), "Reparent back should succeed: {:?}", result);
let final_url = session.get_session_url().unwrap();
assert_eq!(final_url, initial_url);
}
#[test]
fn test_list() {
use std::sync::atomic::{AtomicUsize, Ordering};
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/dir1").unwrap();
root.make_dir("/dir2").unwrap();
root.make_file("/file1.txt").unwrap();
root.make_file("/dir1/file2.txt").unwrap();
let rev = txn.commit().unwrap();
let count = AtomicUsize::new(0);
let callback = |_path: &str, _dirent: &Dirent| {
count.fetch_add(1, Ordering::SeqCst);
Ok(())
};
let result = session.list(
"",
rev,
None,
crate::Depth::Immediates,
crate::DirentField::all(),
&callback,
);
assert!(result.is_ok(), "List should succeed: {:?}", result);
assert_eq!(
count.load(Ordering::SeqCst),
4,
"Should have 4 entries (root + 2 dirs + 1 file)"
);
let subdir_count = AtomicUsize::new(0);
let callback2 = |_path: &str, _dirent: &Dirent| {
subdir_count.fetch_add(1, Ordering::SeqCst);
Ok(())
};
let result = session.list(
"dir1",
rev,
None,
crate::Depth::Infinity,
crate::DirentField::all(),
&callback2,
);
assert!(
result.is_ok(),
"List subdirectory should succeed: {:?}",
result
);
assert_eq!(
subdir_count.load(Ordering::SeqCst),
2,
"Should find dir1 and file2.txt"
);
let empty_count = AtomicUsize::new(0);
let callback3 = |_path: &str, _dirent: &Dirent| {
empty_count.fetch_add(1, Ordering::SeqCst);
Ok(())
};
let result = session.list(
"",
rev,
None,
crate::Depth::Empty,
crate::DirentField::all(),
&callback3,
);
assert!(result.is_ok(), "List with depth=Empty should succeed");
assert_eq!(
empty_count.load(Ordering::SeqCst),
1,
"Depth::Empty should list only the directory itself"
);
}
#[test]
fn test_get_dated_revision() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file1.txt").unwrap();
let rev1 = txn.commit().unwrap();
std::thread::sleep(std::time::Duration::from_millis(100));
let mut txn = fs.begin_txn(rev1, 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file2.txt").unwrap();
let rev2 = txn.commit().unwrap();
let props = fs.revision_proplist(rev1, false).unwrap();
let rev1_date = props.get("svn:date").unwrap();
let rev1_time = crate::time::from_cstring(&String::from_utf8_lossy(rev1_date)).unwrap();
let result = session.get_dated_revision(rev1_time.as_micros());
assert!(
result.is_ok(),
"get_dated_revision should succeed: {:?}",
result
);
assert_eq!(
result.unwrap(),
rev1,
"Should return rev1 for rev1's timestamp"
);
let result = session.get_dated_revision(apr::time::Time::now().as_micros());
assert!(
result.is_ok(),
"get_dated_revision for current time should succeed"
);
assert_eq!(result.unwrap(), rev2, "Should return rev2 for current time");
}
#[test]
fn test_rev_proplist() {
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
let rev = txn.commit().unwrap();
let result = session.rev_proplist(rev);
assert!(result.is_ok(), "rev_proplist should succeed: {:?}", result);
let props = result.unwrap();
assert!(
props.contains_key("svn:date"),
"Should have svn:date property"
);
let date = props.get("svn:date").unwrap();
assert!(!date.is_empty(), "svn:date should not be empty");
}
#[test]
fn test_change_revprop() {
use std::fs;
use std::io::Write;
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
#[allow(unused_mut)]
let mut hook_path = repo.pre_revprop_change_hook_path();
#[cfg(windows)]
hook_path.set_extension("bat");
let mut hook_file = fs::File::create(&hook_path).unwrap();
#[cfg(unix)]
{
writeln!(hook_file, "#!/bin/sh").unwrap();
writeln!(hook_file, "exit 0").unwrap();
}
#[cfg(windows)]
{
writeln!(hook_file, "@exit 0").unwrap();
}
hook_file.sync_all().unwrap();
drop(hook_file); #[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&hook_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&hook_path, perms).unwrap();
}
let fs_obj = repo.fs().unwrap();
let mut txn = fs_obj.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
let rev = txn.commit().unwrap();
let prop_name = "custom:test";
let prop_value = b"test value";
let result = session.change_revprop(rev, prop_name, None, prop_value);
assert!(
result.is_ok(),
"change_revprop should succeed: {:?}",
result
);
let props = session.rev_proplist(rev).unwrap();
assert!(
props.contains_key(prop_name),
"Should have custom:test property"
);
assert_eq!(props.get(prop_name).unwrap(), prop_value);
let result =
session.change_revprop(rev, prop_name, Some(b"wrong old value"), b"another value");
assert!(
result.is_err(),
"change_revprop with wrong old value should fail"
);
}
#[test]
fn test_get_session_url() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
let url = session.get_session_url().unwrap();
assert!(url.starts_with("file://"));
assert!(url.contains("test_repo"));
}
#[test]
fn test_get_commit_editor() {
use crate::delta::{DirectoryEditor, Editor, FileEditor};
use std::collections::HashMap;
let (_temp_dir, repo, mut session, _callbacks) = create_test_repo_with_session();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/initial.txt").unwrap();
let base_rev = txn.commit().unwrap();
let mut revprop_table = HashMap::new();
revprop_table.insert("svn:log".to_string(), b"Test commit via editor".to_vec());
let commit_rev = std::cell::RefCell::new(None);
let commit_callback = |info: &crate::CommitInfo| {
commit_rev.replace(Some(info.revision()));
Ok(())
};
let lock_tokens = HashMap::new();
let result = session.get_commit_editor(revprop_table, &commit_callback, lock_tokens, false);
assert!(result.is_ok(), "get_commit_editor should succeed");
let mut editor = result.unwrap();
let mut root = editor.open_root(Some(base_rev)).unwrap();
let mut file = root.add_file("newfile.txt", None).unwrap();
let _handler = file.apply_textdelta(None).unwrap();
file.close(None).unwrap();
root.close().unwrap();
editor.close().unwrap();
let rev = commit_rev.borrow();
assert!(rev.is_some(), "Commit callback should have been called");
assert!(
rev.unwrap().as_u64() > base_rev.as_u64(),
"Should have created a new revision"
);
}
#[test]
fn test_do_update() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
struct UpdateEditor {
operations: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
}
impl crate::delta::Editor for UpdateEditor {
type RootEditor = UpdateDirEditor;
fn set_target_revision(
&mut self,
revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
eprintln!(
"UpdateEditor::set_target_revision called with {:?}",
revision
);
self.operations
.borrow_mut()
.push(format!("set_target_revision({:?})", revision));
Ok(())
}
fn open_root(
&mut self,
_base_revision: Option<crate::Revnum>,
) -> Result<UpdateDirEditor, crate::Error<'_>> {
eprintln!("UpdateEditor::open_root called");
self.operations.borrow_mut().push("open_root".to_string());
Ok(UpdateDirEditor {
operations: self.operations.clone(),
})
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
self.operations.borrow_mut().push("close".to_string());
Ok(())
}
fn abort(&mut self) -> Result<(), crate::Error<'_>> {
self.operations.borrow_mut().push("abort".to_string());
Ok(())
}
}
struct UpdateDirEditor {
operations: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
}
impl crate::delta::DirectoryEditor for UpdateDirEditor {
type SubDirectory = UpdateDirEditor;
type File = UpdateFileEditor;
fn delete_entry(
&mut self,
path: &str,
_revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("delete_entry({})", path));
Ok(())
}
fn add_directory(
&mut self,
path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<UpdateDirEditor, crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("add_directory({})", path));
Ok(UpdateDirEditor {
operations: self.operations.clone(),
})
}
fn open_directory(
&mut self,
path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<UpdateDirEditor, crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("open_directory({})", path));
Ok(UpdateDirEditor {
operations: self.operations.clone(),
})
}
fn change_prop(
&mut self,
name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("change_prop({})", name));
Ok(())
}
fn add_file(
&mut self,
path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<UpdateFileEditor, crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("add_file({})", path));
Ok(UpdateFileEditor {
operations: self.operations.clone(),
})
}
fn open_file(
&mut self,
path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<UpdateFileEditor, crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("open_file({})", path));
Ok(UpdateFileEditor {
operations: self.operations.clone(),
})
}
fn absent_file(&mut self, path: &str) -> Result<(), crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("absent_file({})", path));
Ok(())
}
fn absent_directory(&mut self, path: &str) -> Result<(), crate::Error<'_>> {
self.operations
.borrow_mut()
.push(format!("absent_directory({})", path));
Ok(())
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
self.operations
.borrow_mut()
.push("close_directory".to_string());
Ok(())
}
}
struct UpdateFileEditor {
operations: std::rc::Rc<std::cell::RefCell<Vec<String>>>,
}
impl crate::delta::FileEditor for UpdateFileEditor {
fn apply_textdelta(
&mut self,
_base_checksum: Option<&str>,
) -> Result<
Box<
dyn for<'b> Fn(
&'b mut crate::delta::TxDeltaWindow,
) -> Result<(), crate::Error<'static>>,
>,
crate::Error<'static>,
> {
self.operations
.borrow_mut()
.push("apply_textdelta".to_string());
Ok(Box::new(|_window| Ok(())))
}
fn change_prop(
&mut self,
name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'static>> {
self.operations
.borrow_mut()
.push(format!("file_change_prop({})", name));
Ok(())
}
fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
self.operations.borrow_mut().push("close_file".to_string());
Ok(())
}
}
let operations = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let update_editor = UpdateEditor {
operations: operations.clone(),
};
let mut editor = crate::delta::WrapEditor::from_rust_editor(update_editor);
let result = session.do_update(
crate::Revnum(0),
"",
crate::Depth::Infinity,
false,
false,
&mut editor,
);
assert!(result.is_ok(), "do_update should succeed");
let mut reporter = result.unwrap();
reporter.abort_report().unwrap();
}
#[test]
fn test_do_switch() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
struct SwitchEditor;
impl crate::delta::Editor for SwitchEditor {
type RootEditor = SwitchDirEditor;
fn set_target_revision(
&mut self,
_revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn open_root(
&mut self,
_base_revision: Option<crate::Revnum>,
) -> Result<SwitchDirEditor, crate::Error<'_>> {
Ok(SwitchDirEditor)
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn abort(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
}
struct SwitchDirEditor;
impl crate::delta::DirectoryEditor for SwitchDirEditor {
type SubDirectory = SwitchDirEditor;
type File = SwitchFileEditor;
fn delete_entry(
&mut self,
_path: &str,
_revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn add_directory(
&mut self,
_path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<SwitchDirEditor, crate::Error<'_>> {
Ok(SwitchDirEditor)
}
fn open_directory(
&mut self,
_path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<SwitchDirEditor, crate::Error<'_>> {
Ok(SwitchDirEditor)
}
fn change_prop(
&mut self,
_name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn add_file(
&mut self,
_path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<SwitchFileEditor, crate::Error<'_>> {
Ok(SwitchFileEditor)
}
fn open_file(
&mut self,
_path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<SwitchFileEditor, crate::Error<'_>> {
Ok(SwitchFileEditor)
}
fn absent_file(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn absent_directory(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
}
struct SwitchFileEditor;
impl crate::delta::FileEditor for SwitchFileEditor {
fn apply_textdelta(
&mut self,
_base_checksum: Option<&str>,
) -> Result<
Box<
dyn for<'b> Fn(
&'b mut crate::delta::TxDeltaWindow,
) -> Result<(), crate::Error<'static>>,
>,
crate::Error<'static>,
> {
Ok(Box::new(|_window| Ok(())))
}
fn change_prop(
&mut self,
_name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'static>> {
Ok(())
}
fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
Ok(())
}
}
let switch_editor = SwitchEditor;
let mut editor = crate::delta::WrapEditor::from_rust_editor(switch_editor);
let url = session.get_session_url().unwrap();
let result = session.do_switch(
crate::Revnum(0),
"",
crate::Depth::Infinity,
&url,
false,
false,
&mut editor,
);
assert!(result.is_ok(), "do_switch should succeed");
let mut reporter = result.unwrap();
reporter.abort_report().unwrap();
}
#[test]
fn test_do_status() {
let (_temp_dir, _repo, mut session, _callbacks) = create_test_repo_with_session();
struct StatusEditor;
impl crate::delta::Editor for StatusEditor {
type RootEditor = StatusDirEditor;
fn set_target_revision(
&mut self,
_revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn open_root(
&mut self,
_base_revision: Option<crate::Revnum>,
) -> Result<StatusDirEditor, crate::Error<'_>> {
Ok(StatusDirEditor)
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn abort(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
}
struct StatusDirEditor;
impl crate::delta::DirectoryEditor for StatusDirEditor {
type SubDirectory = StatusDirEditor;
type File = StatusFileEditor;
fn delete_entry(
&mut self,
_path: &str,
_revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn add_directory(
&mut self,
_path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<StatusDirEditor, crate::Error<'_>> {
Ok(StatusDirEditor)
}
fn open_directory(
&mut self,
_path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<StatusDirEditor, crate::Error<'_>> {
Ok(StatusDirEditor)
}
fn change_prop(
&mut self,
_name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn add_file(
&mut self,
_path: &str,
_copyfrom: Option<(&str, crate::Revnum)>,
) -> Result<StatusFileEditor, crate::Error<'_>> {
Ok(StatusFileEditor)
}
fn open_file(
&mut self,
_path: &str,
_base_revision: Option<crate::Revnum>,
) -> Result<StatusFileEditor, crate::Error<'_>> {
Ok(StatusFileEditor)
}
fn absent_file(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn absent_directory(&mut self, _path: &str) -> Result<(), crate::Error<'_>> {
Ok(())
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
Ok(())
}
}
struct StatusFileEditor;
impl crate::delta::FileEditor for StatusFileEditor {
fn apply_textdelta(
&mut self,
_base_checksum: Option<&str>,
) -> Result<
Box<
dyn for<'b> Fn(
&'b mut crate::delta::TxDeltaWindow,
) -> Result<(), crate::Error<'static>>,
>,
crate::Error<'static>,
> {
Ok(Box::new(|_window| Ok(())))
}
fn change_prop(
&mut self,
_name: &str,
_value: Option<&[u8]>,
) -> Result<(), crate::Error<'static>> {
Ok(())
}
fn close(&mut self, _text_checksum: Option<&str>) -> Result<(), crate::Error<'static>> {
Ok(())
}
}
let status_editor = StatusEditor;
let mut editor = crate::delta::WrapEditor::from_rust_editor(status_editor);
let result = session.do_status("", crate::Revnum(0), crate::Depth::Infinity, &mut editor);
assert!(result.is_ok(), "do_status should succeed");
}
#[test]
fn test_callbacks_set_cancel_func() {
let mut callbacks = Callbacks::new().unwrap();
let cancel_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let cancel_called_clone = cancel_called.clone();
callbacks.set_cancel_func(move || {
cancel_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
});
unsafe {
assert!((*callbacks.ptr).cancel_func.is_some());
}
}
#[test]
fn test_callbacks_set_progress_func() {
let mut callbacks = Callbacks::new().unwrap();
let progress_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let progress_called_clone = progress_called.clone();
callbacks.set_progress_func(move |_progress, _total| {
progress_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
});
unsafe {
assert!((*callbacks.ptr).progress_func.is_some());
assert!(!(*callbacks.ptr).progress_baton.is_null());
}
}
#[test]
fn test_wrap_reporter_from_rust_reporter() {
use std::sync::{Arc, Mutex};
struct TestReporter {
calls: Arc<Mutex<Vec<String>>>,
}
impl Reporter for TestReporter {
fn set_path(
&mut self,
path: &str,
_rev: crate::Revnum,
_depth: crate::Depth,
_start_empty: bool,
_lock_token: &str,
) -> Result<(), crate::Error<'static>> {
self.calls
.lock()
.unwrap()
.push(format!("set_path:{}", path));
Ok(())
}
fn delete_path(&mut self, path: &str) -> Result<(), crate::Error<'static>> {
self.calls
.lock()
.unwrap()
.push(format!("delete_path:{}", path));
Ok(())
}
fn link_path(
&mut self,
path: &str,
url: &str,
_rev: crate::Revnum,
_depth: crate::Depth,
_start_empty: bool,
_lock_token: &str,
) -> Result<(), crate::Error<'static>> {
self.calls
.lock()
.unwrap()
.push(format!("link_path:{}:{}", path, url));
Ok(())
}
fn finish_report(&mut self) -> Result<(), crate::Error<'static>> {
self.calls.lock().unwrap().push("finish_report".to_string());
Ok(())
}
fn abort_report(&mut self) -> Result<(), crate::Error<'static>> {
self.calls.lock().unwrap().push("abort_report".to_string());
Ok(())
}
}
let calls = Arc::new(Mutex::new(Vec::new()));
let reporter = TestReporter {
calls: calls.clone(),
};
let wrap = WrapReporter::from_rust_reporter(reporter);
assert!(!wrap.as_ptr().is_null());
assert!(!wrap.as_baton().is_null());
unsafe {
let vtable = &*wrap.as_ptr();
let path = std::ffi::CString::new("test/path").unwrap();
let lock_token = std::ffi::CString::new("").unwrap();
let pool = apr::Pool::new();
let err = vtable.set_path.unwrap()(
wrap.as_baton(),
path.as_ptr(),
1,
subversion_sys::svn_depth_t_svn_depth_infinity,
0,
lock_token.as_ptr(),
pool.as_mut_ptr(),
);
assert!(err.is_null());
let err =
vtable.delete_path.unwrap()(wrap.as_baton(), path.as_ptr(), pool.as_mut_ptr());
assert!(err.is_null());
let err = vtable.finish_report.unwrap()(wrap.as_baton(), pool.as_mut_ptr());
assert!(err.is_null());
}
let recorded = calls.lock().unwrap();
assert_eq!(recorded.len(), 3);
assert_eq!(recorded[0], "set_path:test/path");
assert_eq!(recorded[1], "delete_path:test/path");
assert_eq!(recorded[2], "finish_report");
}
}