use crate::{svn_result, with_tmp_pool, Error, Revnum};
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
fn box_pack_notify_baton(f: Box<dyn Fn(&str) + Send>) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_verify_notify_baton(f: Box<dyn Fn(Revnum, &str) + Send>) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_cancel_baton(f: Box<dyn Fn() -> bool + Send>) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
unsafe fn free_pack_notify_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(baton as *mut Box<dyn Fn(&str) + Send>));
}
unsafe fn free_verify_notify_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton as *mut Box<dyn Fn(Revnum, &str) + Send>,
));
}
unsafe fn free_cancel_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(baton as *mut Box<dyn Fn() -> bool + Send>));
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FsPath {
path: CString,
}
impl FsPath {
pub fn from_canonical(path: &str) -> Result<Self, Error<'static>> {
let path = if path.is_empty() { "/" } else { path };
if !path.starts_with('/') {
return Err(Error::from_message(&format!(
"Filesystem path must be absolute (start with '/'): {}",
path
)));
}
with_tmp_pool(|pool| unsafe {
let path_cstr = CString::new(path)?;
let is_canonical =
subversion_sys::svn_path_is_canonical(path_cstr.as_ptr(), pool.as_mut_ptr()) != 0;
if is_canonical {
Ok(Self { path: path_cstr })
} else {
Err(Error::from_message(&format!(
"Path is not canonical: {}",
path
)))
}
})
}
pub fn canonicalize(path: &str) -> Result<Self, Error<'static>> {
let path = if path.is_empty() { "/" } else { path };
if !path.starts_with('/') {
return Err(Error::from_message(&format!(
"Filesystem path must be absolute (start with '/'): {}",
path
)));
}
with_tmp_pool(|pool| unsafe {
let path_cstr = CString::new(path)?;
let is_canonical =
subversion_sys::svn_path_is_canonical(path_cstr.as_ptr(), pool.as_mut_ptr()) != 0;
if is_canonical {
Ok(Self { path: path_cstr })
} else {
let canonical_ptr =
subversion_sys::svn_path_canonicalize(path_cstr.as_ptr(), pool.as_mut_ptr());
if canonical_ptr.is_null() {
return Err(Error::from_message(&format!(
"Failed to canonicalize path: {}",
path
)));
}
let canonical_str = CStr::from_ptr(canonical_ptr).to_str()?;
Ok(Self {
path: CString::new(canonical_str)?,
})
}
})
}
pub fn as_ptr(&self) -> *const i8 {
self.path.as_ptr()
}
pub fn as_str(&self) -> &str {
self.path.to_str().unwrap_or("/")
}
}
impl TryFrom<&str> for FsPath {
type Error = Error<'static>;
fn try_from(path: &str) -> Result<Self, Self::Error> {
FsPath::canonicalize(path)
}
}
impl TryFrom<String> for FsPath {
type Error = Error<'static>;
fn try_from(path: String) -> Result<Self, Self::Error> {
FsPath::canonicalize(&path)
}
}
impl std::fmt::Display for FsPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
pub struct FsPathChange {
ptr: *const subversion_sys::svn_fs_path_change2_t,
}
impl FsPathChange {
pub fn from_raw(ptr: *mut subversion_sys::svn_fs_path_change2_t) -> Self {
Self { ptr }
}
pub fn change_kind(&self) -> crate::FsPathChangeKind {
unsafe { (*self.ptr).change_kind.into() }
}
pub fn node_kind(&self) -> crate::NodeKind {
unsafe { (*self.ptr).node_kind.into() }
}
pub fn text_modified(&self) -> bool {
unsafe { (*self.ptr).text_mod != 0 }
}
pub fn props_modified(&self) -> bool {
unsafe { (*self.ptr).prop_mod != 0 }
}
pub fn copyfrom_path(&self) -> Option<String> {
unsafe {
if (*self.ptr).copyfrom_path.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).copyfrom_path)
.to_string_lossy()
.into_owned(),
)
}
}
}
pub fn copyfrom_rev(&self) -> Option<Revnum> {
unsafe {
let rev = (*self.ptr).copyfrom_rev;
if rev == -1 {
None
} else {
Some(Revnum(rev))
}
}
}
}
pub struct FsPathChange3<'a> {
ptr: *const subversion_sys::svn_fs_path_change3_t,
_marker: PhantomData<&'a ()>,
}
impl<'a> FsPathChange3<'a> {
unsafe fn from_raw(ptr: *const subversion_sys::svn_fs_path_change3_t) -> Self {
Self {
ptr,
_marker: PhantomData,
}
}
pub fn path(&self) -> &str {
unsafe {
let svn_string = &(*self.ptr).path;
std::str::from_utf8_unchecked(std::slice::from_raw_parts(
svn_string.data as *const u8,
svn_string.len,
))
}
}
pub fn change_kind(&self) -> crate::FsPathChangeKind {
unsafe { (*self.ptr).change_kind.into() }
}
pub fn node_kind(&self) -> crate::NodeKind {
unsafe { (*self.ptr).node_kind.into() }
}
pub fn text_modified(&self) -> bool {
unsafe { (*self.ptr).text_mod != 0 }
}
pub fn props_modified(&self) -> bool {
unsafe { (*self.ptr).prop_mod != 0 }
}
pub fn mergeinfo_modified(&self) -> bool {
unsafe { (*self.ptr).mergeinfo_mod as i32 != 0 }
}
pub fn copyfrom_path(&self) -> Option<&str> {
unsafe {
if (*self.ptr).copyfrom_known == 0 || (*self.ptr).copyfrom_path.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).copyfrom_path)
.to_str()
.unwrap(),
)
}
}
}
pub fn copyfrom_rev(&self) -> Option<Revnum> {
unsafe {
if (*self.ptr).copyfrom_known == 0 {
None
} else {
let rev = (*self.ptr).copyfrom_rev;
if rev == -1 {
None
} else {
Some(Revnum(rev))
}
}
}
}
}
pub struct PathChangeIterator<'a> {
iter_ptr: *mut subversion_sys::svn_fs_path_change_iterator_t,
_marker: PhantomData<&'a ()>,
}
impl<'a> PathChangeIterator<'a> {
unsafe fn from_raw(iter_ptr: *mut subversion_sys::svn_fs_path_change_iterator_t) -> Self {
Self {
iter_ptr,
_marker: PhantomData,
}
}
}
impl<'a> Iterator for PathChangeIterator<'a> {
type Item = Result<FsPathChange3<'a>, Error<'static>>;
fn next(&mut self) -> Option<Self::Item> {
unsafe {
let mut change_ptr: *mut subversion_sys::svn_fs_path_change3_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_path_change_get(&mut change_ptr, self.iter_ptr);
if let Err(e) = svn_result(err) {
return Some(Err(e));
}
if change_ptr.is_null() {
None
} else {
Some(Ok(FsPathChange3::from_raw(change_ptr)))
}
}
}
}
pub struct FsDirEntry {
ptr: *const subversion_sys::svn_fs_dirent_t,
_pool: apr::SharedPool<'static>,
}
impl FsDirEntry {
pub fn from_raw(
ptr: *mut subversion_sys::svn_fs_dirent_t,
pool: apr::SharedPool<'static>,
) -> Self {
Self { ptr, _pool: pool }
}
pub fn name(&self) -> &str {
unsafe { std::ffi::CStr::from_ptr((*self.ptr).name).to_str().unwrap() }
}
pub fn id(&self) -> Option<Vec<u8>> {
unsafe {
if (*self.ptr).id.is_null() {
None
} else {
let id_str = subversion_sys::svn_fs_unparse_id(
(*self.ptr).id,
apr::Pool::new().as_mut_ptr(),
);
if id_str.is_null() {
None
} else {
let len = (*id_str).len;
let data = (*id_str).data;
Some(std::slice::from_raw_parts(data as *const u8, len).to_vec())
}
}
}
}
pub fn kind(&self) -> crate::NodeKind {
unsafe { (*self.ptr).kind.into() }
}
}
pub struct Fs<'pool> {
fs_ptr: *mut subversion_sys::svn_fs_t,
pool: apr::Pool<'pool>, _warning_baton: Option<Box<Box<dyn Fn(&Error<'static>) + Send>>>,
}
unsafe impl Send for Fs<'_> {}
impl Drop for Fs<'_> {
fn drop(&mut self) {
}
}
unsafe extern "C" fn warning_func_trampoline(
baton: *mut std::ffi::c_void,
err: *mut subversion_sys::svn_error_t,
) {
if baton.is_null() || err.is_null() {
return;
}
let cb = &*(baton as *const Box<dyn Fn(&Error<'static>) + Send>);
let error = Error::from_ptr_borrowed(err);
cb(&error);
}
unsafe extern "C" fn process_contents_trampoline(
contents: *const std::os::raw::c_uchar,
len: apr_sys::apr_size_t,
baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || contents.is_null() {
return std::ptr::null_mut(); }
let cb = &mut *(baton as *mut Box<dyn FnMut(&[u8]) -> Result<(), Error<'static>>>);
let slice = std::slice::from_raw_parts(contents, len);
match cb(slice) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => e.detach(), }
}
impl<'pool> Fs<'pool> {
pub fn pool(&self) -> &apr::Pool<'_> {
&self.pool
}
pub fn as_ptr(&self) -> *const subversion_sys::svn_fs_t {
self.fs_ptr
}
pub fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_fs_t {
self.fs_ptr
}
pub(crate) unsafe fn from_ptr_and_pool(
fs_ptr: *mut subversion_sys::svn_fs_t,
pool: apr::Pool<'pool>,
) -> Self {
Self {
fs_ptr,
pool,
_warning_baton: None,
}
}
pub fn create(path: &std::path::Path) -> Result<Fs<'static>, Error<'_>> {
crate::init::initialize()?;
let pool = apr::Pool::new();
let path_str = path
.to_str()
.ok_or_else(|| Error::from_message("Invalid path"))?;
let path_c = std::ffi::CString::new(path_str)
.map_err(|_| Error::from_message("Invalid path string"))?;
unsafe {
let mut fs_ptr = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = subversion_sys::svn_fs_create2(
&mut fs_ptr,
path_c.as_ptr(),
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
})?;
Ok(Fs {
fs_ptr,
pool,
_warning_baton: None,
})
}
}
pub fn open(path: &std::path::Path) -> Result<Fs<'static>, Error<'_>> {
crate::init::initialize()?;
let pool = apr::Pool::new();
let path_str = path
.to_str()
.ok_or_else(|| Error::from_message("Invalid path"))?;
let path_c = std::ffi::CString::new(path_str)
.map_err(|_| Error::from_message("Invalid path string"))?;
unsafe {
let mut fs_ptr = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = subversion_sys::svn_fs_open2(
&mut fs_ptr,
path_c.as_ptr(),
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
})?;
Ok(Fs {
fs_ptr,
pool,
_warning_baton: None,
})
}
}
pub fn path(&self) -> std::path::PathBuf {
unsafe {
with_tmp_pool(|pool| {
let path = subversion_sys::svn_fs_path(self.fs_ptr, pool.as_mut_ptr());
std::ffi::CStr::from_ptr(path)
.to_string_lossy()
.into_owned()
.into()
})
}
}
pub fn config(&self) -> std::collections::HashMap<String, String> {
let pool = apr::Pool::new();
let mut result = std::collections::HashMap::new();
unsafe {
let hash_ptr = subversion_sys::svn_fs_config(self.fs_ptr, pool.as_mut_ptr());
if hash_ptr.is_null() {
return result;
}
let mut hi = apr_sys::apr_hash_first(pool.as_mut_ptr(), hash_ptr);
while !hi.is_null() {
let mut key_ptr: *const std::ffi::c_void = std::ptr::null();
let mut key_len: apr_sys::apr_ssize_t = 0;
let mut val_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
apr_sys::apr_hash_this(hi, &mut key_ptr, &mut key_len, &mut val_ptr);
let key_str = if key_len < 0 {
std::ffi::CStr::from_ptr(key_ptr as *const std::os::raw::c_char)
.to_string_lossy()
.into_owned()
} else {
String::from_utf8_lossy(std::slice::from_raw_parts(
key_ptr as *const u8,
key_len as usize,
))
.into_owned()
};
let val_str = if val_ptr.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(val_ptr as *const std::os::raw::c_char)
.to_string_lossy()
.into_owned()
};
result.insert(key_str, val_str);
hi = apr_sys::apr_hash_next(hi);
}
}
result
}
pub fn set_warning_func<F>(&mut self, f: F)
where
F: Fn(&Error<'static>) + Send + 'static,
{
let boxed: Box<Box<dyn Fn(&Error<'static>) + Send>> = Box::new(Box::new(f));
let baton_ptr = Box::into_raw(boxed) as *mut std::ffi::c_void;
unsafe {
subversion_sys::svn_fs_set_warning_func(
self.fs_ptr,
Some(warning_func_trampoline),
baton_ptr,
);
}
self._warning_baton = Some(unsafe { Box::from_raw(baton_ptr as *mut _) });
}
pub fn youngest_revision(&self) -> Result<Revnum, Error<'static>> {
unsafe {
with_tmp_pool(|pool| {
let mut youngest = 0;
let err = subversion_sys::svn_fs_youngest_rev(
&mut youngest,
self.fs_ptr,
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Revnum::from_raw(youngest).unwrap())
})
}
}
pub fn revision_proplist(
&self,
rev: Revnum,
refresh: bool,
) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
let result_pool = apr::pool::Pool::new();
let scratch_pool = apr::pool::Pool::new();
let mut props = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_fs_revision_proplist2(
&mut props,
self.fs_ptr,
rev.0,
refresh as i32,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)?;
if props.is_null() {
return Ok(std::collections::HashMap::new());
}
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
let revprops = prop_hash.to_hashmap();
Ok(revprops)
}
pub fn revision_prop(
&self,
rev: Revnum,
propname: &str,
refresh: bool,
) -> Result<Option<Vec<u8>>, Error<'static>> {
let name_cstr = std::ffi::CString::new(propname)?;
let result_pool = apr::pool::Pool::new();
let scratch_pool = apr::pool::Pool::new();
unsafe {
let mut value_p: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_revision_prop2(
&mut value_p,
self.fs_ptr,
rev.0,
name_cstr.as_ptr(),
refresh as i32,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)?;
if value_p.is_null() {
return Ok(None);
}
let s = &*value_p;
let data = std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec();
Ok(Some(data))
}
}
pub fn revision_root(&self, rev: Revnum) -> Result<Root<'_>, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut root_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_revision_root(
&mut root_ptr,
self.fs_ptr,
rev.0,
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Root {
ptr: root_ptr,
pool: apr::PoolHandle::owned(pool),
_marker: std::marker::PhantomData,
})
}
}
pub fn get_uuid(&self) -> Result<String, Error<'static>> {
unsafe {
with_tmp_pool(|pool| {
let mut uuid = std::ptr::null();
let err =
subversion_sys::svn_fs_get_uuid(self.fs_ptr, &mut uuid, pool.as_mut_ptr());
svn_result(err)?;
Ok(std::ffi::CStr::from_ptr(uuid)
.to_string_lossy()
.into_owned())
})
}
}
pub fn set_uuid(&mut self, uuid: &str) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
unsafe {
let uuid = std::ffi::CString::new(uuid).unwrap();
let err = subversion_sys::svn_fs_set_uuid(
self.fs_ptr,
uuid.as_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(())
}
}
pub fn lock(
&mut self,
path: &str,
token: Option<&str>,
comment: Option<&str>,
is_dav_comment: bool,
expiration_date: Option<i64>,
current_rev: Revnum,
steal_lock: bool,
) -> Result<crate::Lock<'static>, Error<'_>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let token_cstr = token.map(|t| std::ffi::CString::new(t).unwrap());
let token_ptr = token_cstr.as_ref().map_or(std::ptr::null(), |t| t.as_ptr());
let comment_cstr = comment.map(|c| std::ffi::CString::new(c).unwrap());
let comment_ptr = comment_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr());
let mut lock_ptr: *mut subversion_sys::svn_lock_t = std::ptr::null_mut();
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_fs_lock(
&mut lock_ptr,
self.fs_ptr,
path_cstr.as_ptr(),
token_ptr,
comment_ptr,
is_dav_comment as i32,
expiration_date.unwrap_or(0),
current_rev.0,
steal_lock as i32,
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
let pool_handle = apr::PoolHandle::owned(pool);
Ok(crate::Lock::from_raw(lock_ptr, pool_handle))
}
pub fn lock_many(
&mut self,
targets: &[(&str, Option<&str>, Revnum)],
comment: Option<&str>,
is_dav_comment: bool,
expiration_date: Option<i64>,
steal_lock: bool,
mut callback: impl FnMut(&str, Option<Error<'_>>),
) -> Result<(), Error<'static>> {
struct Baton<'a> {
func: &'a mut dyn FnMut(&str, Option<Error<'_>>),
}
unsafe extern "C" fn lock_callback(
baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
_lock: *const subversion_sys::svn_lock_t,
fs_err: *mut subversion_sys::svn_error_t,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = &mut *(baton as *mut Baton<'_>);
let path_str = CStr::from_ptr(path).to_str().unwrap_or("");
let error = if fs_err.is_null() {
None
} else {
crate::svn_result(fs_err).err()
};
(baton.func)(path_str, error);
std::ptr::null_mut()
}
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let result_pool_ptr = result_pool.as_mut_ptr();
let scratch_pool_ptr = scratch_pool.as_mut_ptr();
let comment_cstr = comment.map(|c| std::ffi::CString::new(c).unwrap());
let comment_ptr = comment_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr());
let path_cstrings: Vec<std::ffi::CString> = targets
.iter()
.map(|(p, _, _)| std::ffi::CString::new(*p).unwrap())
.collect();
let token_cstrings: Vec<Option<std::ffi::CString>> = targets
.iter()
.map(|(_, t, _)| t.map(|t| std::ffi::CString::new(t).unwrap()))
.collect();
unsafe {
let hash = apr_sys::apr_hash_make(scratch_pool_ptr);
for (i, (_, _, rev)) in targets.iter().enumerate() {
let token_ptr = token_cstrings[i]
.as_ref()
.map_or(std::ptr::null(), |t| t.as_ptr());
let target =
subversion_sys::svn_fs_lock_target_create(token_ptr, rev.0, result_pool_ptr);
apr_sys::apr_hash_set(
hash,
path_cstrings[i].as_ptr() as *const std::ffi::c_void,
apr_sys::APR_HASH_KEY_STRING as isize,
target as *mut std::ffi::c_void,
);
}
let mut baton = Baton {
func: &mut callback,
};
let err = subversion_sys::svn_fs_lock_many(
self.fs_ptr,
hash,
comment_ptr,
is_dav_comment as i32,
expiration_date.unwrap_or(0),
steal_lock as i32,
Some(lock_callback),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
result_pool_ptr,
scratch_pool_ptr,
);
svn_result(err)
}
}
pub fn unlock_many(
&mut self,
targets: &[(&str, &str)],
break_lock: bool,
mut callback: impl FnMut(&str, Option<Error<'_>>),
) -> Result<(), Error<'static>> {
struct Baton<'a> {
func: &'a mut dyn FnMut(&str, Option<Error<'_>>),
}
unsafe extern "C" fn unlock_callback(
baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
_lock: *const subversion_sys::svn_lock_t,
fs_err: *mut subversion_sys::svn_error_t,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = &mut *(baton as *mut Baton<'_>);
let path_str = CStr::from_ptr(path).to_str().unwrap_or("");
let error = if fs_err.is_null() {
None
} else {
crate::svn_result(fs_err).err()
};
(baton.func)(path_str, error);
std::ptr::null_mut()
}
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let result_pool_ptr = result_pool.as_mut_ptr();
let scratch_pool_ptr = scratch_pool.as_mut_ptr();
let path_cstrings: Vec<std::ffi::CString> = targets
.iter()
.map(|(p, _)| std::ffi::CString::new(*p).unwrap())
.collect();
let token_cstrings: Vec<std::ffi::CString> = targets
.iter()
.map(|(_, t)| std::ffi::CString::new(*t).unwrap())
.collect();
unsafe {
let hash = apr_sys::apr_hash_make(scratch_pool_ptr);
for (i, _) in targets.iter().enumerate() {
apr_sys::apr_hash_set(
hash,
path_cstrings[i].as_ptr() as *const std::ffi::c_void,
apr_sys::APR_HASH_KEY_STRING as isize,
token_cstrings[i].as_ptr() as *mut std::ffi::c_void,
);
}
let mut baton = Baton {
func: &mut callback,
};
let err = subversion_sys::svn_fs_unlock_many(
self.fs_ptr,
hash,
break_lock as i32,
Some(unlock_callback),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
result_pool_ptr,
scratch_pool_ptr,
);
svn_result(err)
}
}
pub fn unlock(
&mut self,
path: &str,
token: &str,
break_lock: bool,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let token_cstr = std::ffi::CString::new(token).unwrap();
let ret = unsafe {
subversion_sys::svn_fs_unlock(
self.fs_ptr,
path_cstr.as_ptr(),
token_cstr.as_ptr(),
break_lock as i32,
apr::Pool::new().as_mut_ptr(),
)
};
svn_result(ret)
}
pub fn get_lock(&self, path: &str) -> Result<Option<crate::Lock<'static>>, Error<'_>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let mut lock_ptr: *mut subversion_sys::svn_lock_t = std::ptr::null_mut();
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_fs_get_lock(
&mut lock_ptr,
self.fs_ptr,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
if lock_ptr.is_null() {
Ok(None)
} else {
let pool_handle = apr::PoolHandle::owned(pool);
Ok(Some(crate::Lock::from_raw(lock_ptr, pool_handle)))
}
}
pub fn set_access(&mut self, username: &str) -> Result<(), Error<'static>> {
let username_cstr = std::ffi::CString::new(username).unwrap();
let mut access_ctx: *mut subversion_sys::svn_fs_access_t = std::ptr::null_mut();
let ret = unsafe {
subversion_sys::svn_fs_create_access(
&mut access_ctx,
username_cstr.as_ptr(),
self.pool.as_mut_ptr(),
)
};
svn_result(ret)?;
let ret = unsafe { subversion_sys::svn_fs_set_access(self.fs_ptr, access_ctx) };
svn_result(ret)
}
pub fn access_add_lock_token(&mut self, path: &str, token: &str) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let token_cstr = std::ffi::CString::new(token).unwrap();
let mut access_ctx: *mut subversion_sys::svn_fs_access_t = std::ptr::null_mut();
let ret = unsafe { subversion_sys::svn_fs_get_access(&mut access_ctx, self.fs_ptr) };
svn_result(ret)?;
if access_ctx.is_null() {
return Err(Error::new(
apr::Status::from(subversion_sys::svn_errno_t_SVN_ERR_FS_NO_USER as i32),
None,
"No access context set",
));
}
let ret = unsafe {
subversion_sys::svn_fs_access_add_lock_token2(
access_ctx,
path_cstr.as_ptr(),
token_cstr.as_ptr(),
)
};
svn_result(ret)
}
pub fn get_access_username(&self) -> Result<Option<String>, Error<'static>> {
with_tmp_pool(|pool| {
let mut access_ctx: *mut subversion_sys::svn_fs_access_t = std::ptr::null_mut();
let ret = unsafe { subversion_sys::svn_fs_get_access(&mut access_ctx, self.fs_ptr) };
svn_result(ret)?;
if access_ctx.is_null() {
return Ok(None);
}
let mut username: *const std::os::raw::c_char = std::ptr::null();
let ret =
unsafe { subversion_sys::svn_fs_access_get_username(&mut username, access_ctx) };
let _ = pool; svn_result(ret)?;
if username.is_null() {
return Ok(None);
}
Ok(Some(
unsafe { std::ffi::CStr::from_ptr(username) }
.to_string_lossy()
.into_owned(),
))
})
}
pub fn generate_lock_token(&self) -> Result<String, Error<'static>> {
let pool = apr::Pool::new();
let mut token_ptr: *const std::os::raw::c_char = std::ptr::null();
let ret = unsafe {
subversion_sys::svn_fs_generate_lock_token(
&mut token_ptr,
self.fs_ptr,
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
let token = unsafe {
std::ffi::CStr::from_ptr(token_ptr)
.to_str()
.unwrap()
.to_string()
};
Ok(token)
}
pub fn get_locks(
&self,
path: &str,
depth: crate::Depth,
) -> Result<Vec<crate::Lock<'static>>, Error<'_>> {
let pool = apr::Pool::new();
let path_cstr = std::ffi::CString::new(path).unwrap();
let mut locks = Vec::new();
let locks_ptr = &mut locks as *mut Vec<crate::Lock<'static>> as *mut std::ffi::c_void;
extern "C" fn lock_callback(
baton: *mut std::ffi::c_void,
lock: *mut subversion_sys::svn_lock_t,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let locks = &mut *(baton as *mut Vec<crate::Lock<'static>>);
if !lock.is_null() {
let pool_handle = apr::PoolHandle::from_borrowed_raw(pool);
locks.push(crate::Lock::from_raw(lock, pool_handle));
}
}
std::ptr::null_mut()
}
let ret = unsafe {
subversion_sys::svn_fs_get_locks2(
self.fs_ptr,
path_cstr.as_ptr(),
depth.into(),
Some(lock_callback),
locks_ptr,
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
Ok(locks)
}
pub fn freeze<F>(&mut self, freeze_func: F) -> Result<(), Error<'static>>
where
F: FnOnce() -> Result<(), Error<'static>>,
{
struct FreezeWrapper<F> {
func: F,
error: Option<Error<'static>>,
}
extern "C" fn freeze_callback<F>(
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t
where
F: FnOnce() -> Result<(), Error<'static>>,
{
unsafe {
let wrapper = &mut *(baton as *mut FreezeWrapper<F>);
let func = std::ptr::read(&wrapper.func as *const F);
match func() {
Ok(()) => std::ptr::null_mut(),
Err(e) => {
wrapper.error = Some(e.clone());
e.into_raw()
}
}
}
}
let mut wrapper = FreezeWrapper {
func: freeze_func,
error: None,
};
let ret = unsafe {
subversion_sys::svn_fs_freeze(
self.fs_ptr,
Some(freeze_callback::<F>),
&mut wrapper as *mut _ as *mut std::ffi::c_void,
self.pool.as_mut_ptr(),
)
};
if let Some(err) = wrapper.error {
return Err(err);
}
svn_result(ret)
}
pub fn info(&self) -> Result<FsInfo, Error<'static>> {
let pool = apr::Pool::new();
let mut info_ptr: *const subversion_sys::svn_fs_info_placeholder_t = std::ptr::null();
let ret = unsafe {
subversion_sys::svn_fs_info(
&mut info_ptr,
self.fs_ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
if info_ptr.is_null() {
return Err(Error::from_message("Failed to get filesystem info"));
}
unsafe {
let info = &*info_ptr;
Ok(FsInfo {
fs_type: if info.fs_type.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(info.fs_type)
.to_string_lossy()
.into_owned(),
)
},
})
}
}
pub fn info_format(&self) -> Result<(i32, (i32, i32, i32)), Error<'static>> {
with_tmp_pool(|pool| {
let mut fs_format: std::os::raw::c_int = 0;
let mut supports_version: *mut subversion_sys::svn_version_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_fs_info_format(
&mut fs_format,
&mut supports_version,
self.fs_ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)?;
let version = if supports_version.is_null() {
(0, 0, 0)
} else {
unsafe {
let v = &*supports_version;
(v.major, v.minor, v.patch)
}
};
Ok((fs_format as i32, version))
})
}
pub fn refresh_revision_props(&mut self) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_fs_refresh_revision_props(self.fs_ptr, pool.as_mut_ptr())
};
svn_result(err)
})
}
pub fn change_rev_prop(
&mut self,
rev: Revnum,
name: &str,
value: Option<&[u8]>,
old_value: Option<Option<&[u8]>>,
) -> Result<(), Error<'static>> {
let name_cstr = std::ffi::CString::new(name)?;
with_tmp_pool(|pool| unsafe {
let new_svn_str;
let new_ptr: *const subversion_sys::svn_string_t = match value {
None => std::ptr::null(),
Some(bytes) => {
new_svn_str = subversion_sys::svn_string_t {
data: bytes.as_ptr() as *const std::os::raw::c_char,
len: bytes.len(),
};
&new_svn_str
}
};
let old_svn_str;
let old_inner_ptr: *const subversion_sys::svn_string_t;
let old_ptr: *const *const subversion_sys::svn_string_t = match old_value {
None => std::ptr::null(), Some(None) => {
old_inner_ptr = std::ptr::null();
&old_inner_ptr
}
Some(Some(bytes)) => {
old_svn_str = subversion_sys::svn_string_t {
data: bytes.as_ptr() as *const std::os::raw::c_char,
len: bytes.len(),
};
old_inner_ptr = &old_svn_str;
&old_inner_ptr
}
};
let err = subversion_sys::svn_fs_change_rev_prop2(
self.fs_ptr,
rev.0,
name_cstr.as_ptr(),
old_ptr,
new_ptr,
pool.as_mut_ptr(),
);
svn_result(err)
})
}
pub fn deltify_revision(&self, revision: Revnum) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_fs_deltify_revision(self.fs_ptr, revision.0, pool.as_mut_ptr())
};
svn_result(err)
})
}
}
pub fn fs_type(path: &std::path::Path) -> Result<String, Error<'static>> {
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
unsafe {
let pool = apr::pool::Pool::new();
let mut fs_type = std::ptr::null();
let err = subversion_sys::svn_fs_type(&mut fs_type, path.as_ptr(), pool.as_mut_ptr());
svn_result(err)?;
Ok(std::ffi::CStr::from_ptr(fs_type)
.to_string_lossy()
.into_owned())
}
}
pub fn delete_fs(path: &std::path::Path) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
unsafe {
let err = subversion_sys::svn_fs_delete_fs(path.as_ptr(), scratch_pool.as_mut_ptr());
svn_result(err)?;
Ok(())
}
}
pub fn pack(
path: &std::path::Path,
notify: Option<Box<dyn Fn(&str) + Send>>,
cancel: Option<Box<dyn Fn() -> bool + Send>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let notify_baton = notify.map(|f| box_pack_notify_baton(f));
let cancel_baton = cancel.map(box_cancel_baton);
extern "C" fn notify_wrapper(
baton: *mut std::ffi::c_void,
shard: i64,
_action: subversion_sys::svn_fs_pack_notify_action_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let notify = unsafe { &*(baton as *const Box<dyn Fn(&str) + Send>) };
notify(&format!("Packing shard {}", shard));
}
std::ptr::null_mut()
}
extern "C" fn cancel_wrapper(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let cancel = unsafe { &*(baton as *const Box<dyn Fn() -> bool + Send>) };
if cancel() {
return unsafe { Error::from_message("Operation cancelled").into_raw() };
}
}
std::ptr::null_mut()
}
let err = unsafe {
subversion_sys::svn_fs_pack(
path_cstr.as_ptr(),
notify_baton.map(|_| notify_wrapper as _),
notify_baton.unwrap_or(std::ptr::null_mut()),
cancel_baton.map(|_| cancel_wrapper as _),
cancel_baton.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
if let Some(baton) = notify_baton {
unsafe { free_pack_notify_baton(baton) };
}
if let Some(baton) = cancel_baton {
unsafe { free_cancel_baton(baton) };
}
svn_result(err)?;
Ok(())
}
pub fn verify(
path: &std::path::Path,
start: Option<Revnum>,
end: Option<Revnum>,
notify: Option<Box<dyn Fn(Revnum, &str) + Send>>,
cancel: Option<Box<dyn Fn() -> bool + Send>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let start_rev = start.map(|r| r.0).unwrap_or(0);
let end_rev = end.map(|r| r.0).unwrap_or(-1);
let notify_baton = notify.map(|f| box_verify_notify_baton(f));
let cancel_baton = cancel.map(box_cancel_baton);
extern "C" fn notify_wrapper(
revision: subversion_sys::svn_revnum_t,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) {
if !baton.is_null() {
let notify = unsafe { &*(baton as *const Box<dyn Fn(Revnum, &str) + Send>) };
notify(Revnum(revision), "Verifying");
}
}
extern "C" fn cancel_wrapper(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let cancel = unsafe { &*(baton as *const Box<dyn Fn() -> bool + Send>) };
if cancel() {
return unsafe { Error::from_message("Operation cancelled").into_raw() };
}
}
std::ptr::null_mut()
}
let err = unsafe {
subversion_sys::svn_fs_verify(
path_cstr.as_ptr(),
std::ptr::null_mut(), start_rev,
end_rev,
notify_baton.map(|_| notify_wrapper as _),
notify_baton.unwrap_or(std::ptr::null_mut()),
cancel_baton.map(|_| cancel_wrapper as _),
cancel_baton.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
if let Some(baton) = notify_baton {
unsafe { free_verify_notify_baton(baton) };
}
if let Some(baton) = cancel_baton {
unsafe { free_cancel_baton(baton) };
}
svn_result(err)?;
Ok(())
}
pub fn hotcopy(
src_path: &std::path::Path,
dst_path: &std::path::Path,
clean: bool,
incremental: bool,
notify: Option<Box<dyn Fn(&str) + Send>>,
cancel: Option<Box<dyn Fn() -> bool + Send>>,
) -> Result<(), Error<'static>> {
let src_cstr = std::ffi::CString::new(src_path.to_str().unwrap()).unwrap();
let dst_cstr = std::ffi::CString::new(dst_path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let notify_baton = notify.map(|f| box_pack_notify_baton(f));
let cancel_baton = cancel.map(box_cancel_baton);
extern "C" fn notify_wrapper(
baton: *mut std::ffi::c_void,
start_revision: subversion_sys::svn_revnum_t,
end_revision: subversion_sys::svn_revnum_t,
_pool: *mut apr_sys::apr_pool_t,
) {
if !baton.is_null() {
let notify = unsafe { &*(baton as *const Box<dyn Fn(&str) + Send>) };
notify(&format!(
"Hotcopy revisions {} to {}",
start_revision, end_revision
));
}
}
extern "C" fn cancel_wrapper(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let cancel = unsafe { &*(baton as *const Box<dyn Fn() -> bool + Send>) };
if cancel() {
return unsafe { Error::from_message("Operation cancelled").into_raw() };
}
}
std::ptr::null_mut()
}
let err = unsafe {
subversion_sys::svn_fs_hotcopy3(
src_cstr.as_ptr(),
dst_cstr.as_ptr(),
clean as i32,
incremental as i32,
notify_baton.map(|_| notify_wrapper as _),
notify_baton.unwrap_or(std::ptr::null_mut()),
cancel_baton.map(|_| cancel_wrapper as _),
cancel_baton.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
if let Some(baton) = notify_baton {
unsafe { free_pack_notify_baton(baton) };
}
if let Some(baton) = cancel_baton {
unsafe { free_cancel_baton(baton) };
}
svn_result(err)?;
Ok(())
}
pub fn recover(
path: &std::path::Path,
cancel: Option<Box<dyn Fn() -> bool + Send>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let cancel_baton = cancel.map(box_cancel_baton);
extern "C" fn cancel_wrapper(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let cancel = unsafe { &*(baton as *const Box<dyn Fn() -> bool + Send>) };
if cancel() {
return unsafe { Error::from_message("Operation cancelled").into_raw() };
}
}
std::ptr::null_mut()
}
let err = unsafe {
subversion_sys::svn_fs_recover(
path_cstr.as_ptr(),
cancel_baton.map(|_| cancel_wrapper as _),
cancel_baton.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
if let Some(baton) = cancel_baton {
unsafe { free_cancel_baton(baton) };
}
svn_result(err)?;
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UpgradeAction {
PackRevprops,
CleanupRevprops,
FormatBumped,
}
pub fn upgrade(
path: &std::path::Path,
notify: Option<Box<dyn Fn(u64, UpgradeAction) + Send>>,
cancel: Option<Box<dyn Fn() -> bool + Send>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let notify_baton = notify.map(|f| Box::into_raw(Box::new(f)) as *mut std::ffi::c_void);
let cancel_baton = cancel.map(box_cancel_baton);
unsafe extern "C" fn notify_wrapper(
baton: *mut std::ffi::c_void,
number: u64,
action: subversion_sys::svn_fs_upgrade_notify_action_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let notify = &*(baton as *const Box<dyn Fn(u64, UpgradeAction) + Send>);
let rust_action = match action {
subversion_sys::svn_fs_upgrade_notify_action_t_svn_fs_upgrade_pack_revprops => {
UpgradeAction::PackRevprops
}
subversion_sys::svn_fs_upgrade_notify_action_t_svn_fs_upgrade_cleanup_revprops => {
UpgradeAction::CleanupRevprops
}
subversion_sys::svn_fs_upgrade_notify_action_t_svn_fs_upgrade_format_bumped => {
UpgradeAction::FormatBumped
}
_ => unreachable!("unknown svn_fs_upgrade_notify_action_t value: {}", action),
};
notify(number, rust_action);
}
std::ptr::null_mut()
}
extern "C" fn cancel_wrapper(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
if !baton.is_null() {
let cancel = unsafe { &*(baton as *const Box<dyn Fn() -> bool + Send>) };
if cancel() {
return unsafe { Error::from_message("Operation cancelled").into_raw() };
}
}
std::ptr::null_mut()
}
let err = unsafe {
subversion_sys::svn_fs_upgrade2(
path_cstr.as_ptr(),
notify_baton.map(|_| notify_wrapper as _),
notify_baton.unwrap_or(std::ptr::null_mut()),
cancel_baton.map(|_| cancel_wrapper as _),
cancel_baton.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
if let Some(baton) = notify_baton {
unsafe {
drop(Box::from_raw(
baton as *mut Box<dyn Fn(u64, UpgradeAction) + Send>,
))
};
}
if let Some(baton) = cancel_baton {
unsafe { free_cancel_baton(baton) };
}
svn_result(err)
}
pub fn version() -> crate::Version {
crate::Version(unsafe { subversion_sys::svn_fs_version() })
}
pub fn print_modules() -> Result<String, crate::Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let buf = subversion_sys::svn_stringbuf_create_empty(pool.as_mut_ptr());
let err = subversion_sys::svn_fs_print_modules(buf, pool.as_mut_ptr());
crate::svn_result(err)?;
if buf.is_null() || (*buf).data.is_null() {
return Ok(String::new());
}
let data = std::slice::from_raw_parts((*buf).data as *const u8, (*buf).len);
Ok(String::from_utf8_lossy(data).into_owned())
}
}
pub fn info_config_files(
path: &std::path::Path,
) -> Result<Vec<std::path::PathBuf>, crate::Error<'static>> {
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
unsafe {
let mut files: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
let mut fs_ptr: *mut subversion_sys::svn_fs_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_open2(
&mut fs_ptr,
path_cstr.as_ptr(),
std::ptr::null_mut(),
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
crate::svn_result(err)?;
let err = subversion_sys::svn_fs_info_config_files(
&mut files,
fs_ptr,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
crate::svn_result(err)?;
if files.is_null() {
return Ok(Vec::new());
}
let nelts = (*files).nelts as usize;
let mut result = Vec::with_capacity(nelts);
for i in 0..nelts {
let elts = (*files).elts as *const *const std::os::raw::c_char;
let s = *elts.add(i);
if !s.is_null() {
let path_str = std::ffi::CStr::from_ptr(s).to_str().unwrap_or("");
result.push(std::path::PathBuf::from(path_str));
}
}
Ok(result)
}
}
#[derive(Debug, Clone)]
pub struct FsInfo {
pub fs_type: Option<String>,
}
pub struct Root<'fs> {
ptr: *mut subversion_sys::svn_fs_root_t,
#[allow(dead_code)]
pool: apr::PoolHandle<'static>,
_marker: std::marker::PhantomData<&'fs ()>,
}
unsafe impl<'fs> Send for Root<'fs> {}
impl<'fs> Root<'fs> {
pub unsafe fn from_raw(
ptr: *mut subversion_sys::svn_fs_root_t,
pool_ptr: *mut apr_sys::apr_pool_t,
) -> Self {
Self {
ptr,
pool: apr::PoolHandle::from_borrowed_raw(pool_ptr),
_marker: std::marker::PhantomData,
}
}
pub fn as_ptr(&self) -> *const subversion_sys::svn_fs_root_t {
self.ptr
}
pub fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_fs_root_t {
self.ptr
}
pub fn fs(&self) -> Fs<'fs> {
let fs_ptr = unsafe { subversion_sys::svn_fs_root_fs(self.ptr) };
let pool = apr::Pool::new();
unsafe { Fs::from_ptr_and_pool(fs_ptr, pool) }
}
pub fn is_dir(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut is_dir = 0;
let err = subversion_sys::svn_fs_is_dir(
&mut is_dir,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(is_dir != 0)
})
}
pub fn is_file(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut is_file = 0;
let err = subversion_sys::svn_fs_is_file(
&mut is_file,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(is_file != 0)
})
}
pub fn file_length(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<i64, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut length = 0;
let err = subversion_sys::svn_fs_file_length(
&mut length,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(length)
})
}
pub fn file_contents(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<crate::io::Stream, Error<'static>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut stream = std::ptr::null_mut();
let err = subversion_sys::svn_fs_file_contents(
&mut stream,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(crate::io::Stream::from_ptr_and_pool(stream, pool))
}
}
pub fn try_process_file_contents<F>(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
processor: F,
) -> Result<bool, Error<'static>>
where
F: FnMut(&[u8]) -> Result<(), Error<'static>>,
{
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
let mut boxed: Box<Box<dyn FnMut(&[u8]) -> Result<(), Error<'static>>>> =
Box::new(Box::new(processor));
let baton_ptr = &mut *boxed as *mut _ as *mut std::ffi::c_void;
unsafe {
let mut success: subversion_sys::svn_boolean_t = 0;
let err = subversion_sys::svn_fs_try_process_file_contents(
&mut success,
self.ptr,
fs_path.as_ptr(),
Some(process_contents_trampoline),
baton_ptr,
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(success != 0)
}
}
pub fn file_checksum(
&self,
path: &str,
kind: crate::ChecksumKind,
) -> Result<Option<crate::Checksum<'_>>, Error<'_>> {
self.file_checksum_force(path, kind, true)
}
pub fn file_checksum_force(
&self,
path: &str,
kind: crate::ChecksumKind,
force: bool,
) -> Result<Option<crate::Checksum<'_>>, Error<'_>> {
with_tmp_pool(|pool| unsafe {
let path_c = std::ffi::CString::new(path).unwrap();
let mut checksum = std::ptr::null_mut();
let err = subversion_sys::svn_fs_file_checksum(
&mut checksum,
kind.into(),
self.ptr,
path_c.as_ptr(),
if force { 1 } else { 0 },
pool.as_mut_ptr(),
);
svn_result(err)?;
if checksum.is_null() {
Ok(None)
} else {
Ok(Some(crate::Checksum::from_raw(checksum)))
}
})
}
pub fn node_prop(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
propname: &str,
) -> Result<Option<Vec<u8>>, Error<'static>> {
let fs_path = path.try_into()?;
let name_cstr = std::ffi::CString::new(propname)?;
with_tmp_pool(|pool| unsafe {
let mut value_p: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_node_prop(
&mut value_p,
self.ptr,
fs_path.as_ptr(),
name_cstr.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if value_p.is_null() {
return Ok(None);
}
let s = &*value_p;
let data = std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec();
Ok(Some(data))
})
}
pub fn proplist(
&self,
path: &str,
) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
with_tmp_pool(|pool| unsafe {
let path_c = std::ffi::CString::new(path).unwrap();
let mut props = std::ptr::null_mut();
let err = subversion_sys::svn_fs_node_proplist(
&mut props,
self.ptr,
path_c.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let result = if !props.is_null() {
let prop_hash = crate::props::PropHash::from_ptr(props);
prop_hash.to_hashmap()
} else {
std::collections::HashMap::new()
};
Ok(result)
})
}
pub fn paths_changed(
&self,
) -> Result<std::collections::HashMap<String, FsPathChange>, Error<'_>> {
with_tmp_pool(|pool| unsafe {
let mut changed_paths = std::ptr::null_mut();
let err = subversion_sys::svn_fs_paths_changed2(
&mut changed_paths,
self.ptr,
pool.as_mut_ptr(),
);
svn_result(err)?;
if changed_paths.is_null() {
Ok(std::collections::HashMap::new())
} else {
let hash = crate::hash::PathChangeHash::from_ptr(changed_paths);
Ok(hash.to_hashmap())
}
})
}
pub fn paths_changed3(&mut self) -> Result<PathChangeIterator<'_>, Error<'static>> {
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let mut iter_ptr: *mut subversion_sys::svn_fs_path_change_iterator_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_fs_paths_changed3(
&mut iter_ptr,
self.ptr,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(unsafe { PathChangeIterator::from_raw(iter_ptr) })
}
pub fn revision(&self) -> Revnum {
Revnum(unsafe { subversion_sys::svn_fs_revision_root_revision(self.ptr) })
}
pub fn is_revision_root(&self) -> bool {
unsafe { subversion_sys::svn_fs_is_revision_root(self.ptr) != 0 }
}
pub fn is_txn_root(&self) -> bool {
unsafe { subversion_sys::svn_fs_is_txn_root(self.ptr) != 0 }
}
pub fn node_created_path(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<String, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut created_path: *const std::os::raw::c_char = std::ptr::null();
let err = subversion_sys::svn_fs_node_created_path(
&mut created_path,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if created_path.is_null() {
return Err(Error::from_message(
"svn_fs_node_created_path returned null",
));
}
Ok(std::ffi::CStr::from_ptr(created_path)
.to_string_lossy()
.into_owned())
})
}
pub fn verify(&self) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let err = unsafe { subversion_sys::svn_fs_verify_root(self.ptr, pool.as_mut_ptr()) };
svn_result(err)
})
}
pub fn get_mergeinfo(
&self,
paths: &[&str],
inherit: crate::mergeinfo::MergeinfoInheritance,
include_descendants: bool,
adjust_inherited_mergeinfo: bool,
mut receiver: impl FnMut(&str, crate::mergeinfo::Mergeinfo) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
struct Baton<'a> {
func:
&'a mut dyn FnMut(&str, crate::mergeinfo::Mergeinfo) -> Result<(), Error<'static>>,
}
unsafe extern "C" fn mergeinfo_trampoline(
path: *const std::os::raw::c_char,
mergeinfo: subversion_sys::svn_mergeinfo_t,
baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = &mut *(baton as *mut Baton<'_>);
let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
let pool = apr::Pool::new();
let dup = subversion_sys::svn_mergeinfo_dup(mergeinfo, pool.as_mut_ptr());
let mi = crate::mergeinfo::Mergeinfo::from_ptr_and_pool(dup, pool);
match (baton.func)(path_str, mi) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
with_tmp_pool(|pool| {
let path_cstrings: Vec<std::ffi::CString> = paths
.iter()
.map(|p| std::ffi::CString::new(*p))
.collect::<Result<_, _>>()?;
let mut arr = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
pool,
path_cstrings.len() as i32,
);
for cstr in &path_cstrings {
arr.push(cstr.as_ptr());
}
let mut baton = Baton {
func: &mut receiver,
};
let err = unsafe {
subversion_sys::svn_fs_get_mergeinfo3(
self.ptr,
arr.as_ptr(),
inherit.into(),
if include_descendants { 1 } else { 0 },
if adjust_inherited_mergeinfo { 1 } else { 0 },
Some(mergeinfo_trampoline),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn check_path(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<crate::NodeKind, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut kind = subversion_sys::svn_node_kind_t_svn_node_none;
let err = subversion_sys::svn_fs_check_path(
&mut kind,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(kind.into())
})
}
pub fn dir_entries(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<std::collections::HashMap<String, FsDirEntry>, Error<'_>> {
let fs_path = path.try_into()?;
let pool = apr::SharedPool::from(apr::Pool::new());
unsafe {
let mut entries = std::ptr::null_mut();
let err = subversion_sys::svn_fs_dir_entries(
&mut entries,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if entries.is_null() {
Ok(std::collections::HashMap::new())
} else {
let hash = crate::hash::FsDirentHash::from_ptr(entries);
Ok(hash.to_hashmap(pool.clone()))
}
}
}
pub fn dir_entries_optimal_order(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<Vec<FsDirEntry>, Error<'_>> {
let fs_path = path.try_into()?;
let result_pool = apr::SharedPool::from(apr::Pool::new());
let scratch_pool = apr::Pool::new();
unsafe {
let mut entries_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_dir_entries(
&mut entries_ptr,
self.ptr,
fs_path.as_ptr(),
result_pool.as_mut_ptr(),
);
svn_result(err)?;
if entries_ptr.is_null() {
return Ok(Vec::new());
}
let mut ordered_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_dir_optimal_order(
&mut ordered_ptr,
self.ptr,
entries_ptr,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)?;
if ordered_ptr.is_null() {
return Ok(Vec::new());
}
let nelts = (*ordered_ptr).nelts as usize;
let elts = (*ordered_ptr).elts as *const *const subversion_sys::svn_fs_dirent_t;
let mut result = Vec::with_capacity(nelts);
for i in 0..nelts {
let entry_ptr = *elts.add(i);
if !entry_ptr.is_null() {
result.push(FsDirEntry::from_raw(
entry_ptr as *mut subversion_sys::svn_fs_dirent_t,
result_pool.clone(),
));
}
}
Ok(result)
}
}
pub fn contents_changed(
&self,
path1: impl TryInto<FsPath, Error = Error<'static>>,
root2: &Root,
path2: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path1 = path1.try_into()?;
let fs_path2 = path2.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut changed: subversion_sys::svn_boolean_t = 0;
let err = subversion_sys::svn_fs_contents_changed(
&mut changed,
self.ptr,
fs_path1.as_ptr(),
root2.ptr,
fs_path2.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(changed != 0)
})
}
pub fn props_changed(
&self,
path1: impl TryInto<FsPath, Error = Error<'static>>,
root2: &Root,
path2: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path1 = path1.try_into()?;
let fs_path2 = path2.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut changed: subversion_sys::svn_boolean_t = 0;
let err = subversion_sys::svn_fs_props_changed(
&mut changed,
self.ptr,
fs_path1.as_ptr(),
root2.ptr,
fs_path2.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(changed != 0)
})
}
pub fn node_history(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<NodeHistory, Error<'static>> {
let fs_path = path.try_into()?;
unsafe {
let pool = apr::Pool::new();
let mut history: *mut subversion_sys::svn_fs_history_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_node_history2(
&mut history,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if history.is_null() {
return Err(Error::from_message("Failed to get node history"));
}
Ok(NodeHistory {
ptr: history,
pool, })
}
}
pub fn node_created_rev(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<Revnum, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut rev: subversion_sys::svn_revnum_t = -1;
let err = subversion_sys::svn_fs_node_created_rev(
&mut rev,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Revnum(rev))
})
}
pub fn node_id(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<NodeId, Error<'static>> {
let fs_path = path.try_into()?;
unsafe {
let pool = apr::Pool::new();
let mut id: *const subversion_sys::svn_fs_id_t = std::ptr::null();
let err = subversion_sys::svn_fs_node_id(
&mut id,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if id.is_null() {
return Err(Error::from_message("Failed to get node ID"));
}
Ok(NodeId { ptr: id, pool })
}
}
pub fn closest_copy(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<Option<(Root<'fs>, String)>, Error<'_>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut copy_root: *mut subversion_sys::svn_fs_root_t = std::ptr::null_mut();
let mut copy_path: *const i8 = std::ptr::null();
let err = subversion_sys::svn_fs_closest_copy(
&mut copy_root,
&mut copy_path,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if copy_root.is_null() {
Ok(None)
} else {
let path_str = std::ffi::CStr::from_ptr(copy_path).to_str()?.to_owned();
Ok(Some((
Root {
ptr: copy_root,
pool: apr::PoolHandle::owned(pool),
_marker: std::marker::PhantomData,
},
path_str,
)))
}
}
}
pub fn contents_different(
&self,
path1: impl TryInto<FsPath, Error = Error<'static>>,
other_root: &Root,
path2: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path1 = path1.try_into()?;
let fs_path2 = path2.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut different: i32 = 0;
let err = subversion_sys::svn_fs_contents_different(
&mut different,
self.ptr,
fs_path1.as_ptr(),
other_root.ptr,
fs_path2.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(different != 0)
}
}
pub fn props_different(
&self,
path1: impl TryInto<FsPath, Error = Error<'static>>,
other_root: &Root,
path2: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path1 = path1.try_into()?;
let fs_path2 = path2.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut different: i32 = 0;
let err = subversion_sys::svn_fs_props_different(
&mut different,
self.ptr,
fs_path1.as_ptr(),
other_root.ptr,
fs_path2.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(different != 0)
}
}
#[cfg(feature = "delta")]
pub fn apply_textdelta(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
base_checksum: Option<&str>,
result_checksum: Option<&str>,
) -> Result<crate::delta::WrapTxdeltaWindowHandler, Error<'static>> {
let fs_path = path.try_into()?;
let base_checksum_cstr = base_checksum.map(std::ffi::CString::new).transpose()?;
let result_checksum_cstr = result_checksum.map(std::ffi::CString::new).transpose()?;
let pool = apr::Pool::new();
unsafe {
let mut handler: subversion_sys::svn_txdelta_window_handler_t = None;
let mut handler_baton: *mut std::ffi::c_void = std::ptr::null_mut();
let err = subversion_sys::svn_fs_apply_textdelta(
&mut handler,
&mut handler_baton,
self.ptr,
fs_path.as_ptr(),
base_checksum_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
result_checksum_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(crate::delta::WrapTxdeltaWindowHandler::from_raw(
handler,
handler_baton,
pool,
))
}
}
pub fn apply_text(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
result_checksum: Option<&str>,
) -> Result<crate::io::Stream, Error<'static>> {
let fs_path = path.try_into()?;
let result_checksum_cstr = result_checksum.map(std::ffi::CString::new).transpose()?;
let pool = apr::Pool::new();
unsafe {
let mut stream: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_apply_text(
&mut stream,
self.ptr,
fs_path.as_ptr(),
result_checksum_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(crate::io::Stream::from_ptr_and_pool(stream, pool))
}
}
#[cfg(feature = "delta")]
pub fn get_file_delta_stream(
&self,
source_path: impl TryInto<FsPath, Error = Error<'static>>,
target_root: &Root,
target_path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<crate::delta::TxDeltaStream, Error<'static>> {
let source_fs_path = source_path.try_into()?;
let target_fs_path = target_path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut stream: *mut subversion_sys::svn_txdelta_stream_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_get_file_delta_stream(
&mut stream,
self.ptr,
source_fs_path.as_ptr(),
target_root.ptr,
target_fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(crate::delta::TxDeltaStream::from_raw(stream, pool))
}
}
pub fn node_has_props(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<bool, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut has_props: subversion_sys::svn_boolean_t = 0;
let err = subversion_sys::svn_fs_node_has_props(
&mut has_props,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(has_props != 0)
})
}
pub fn node_origin_rev(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<Revnum, Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut revision: subversion_sys::svn_revnum_t = -1; let err = subversion_sys::svn_fs_node_origin_rev(
&mut revision,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Revnum(revision))
})
}
pub fn copied_from(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<Option<(Revnum, String)>, Error<'static>> {
let fs_path = path.try_into()?;
let result_pool = apr::Pool::new();
unsafe {
let mut rev: subversion_sys::svn_revnum_t = -1; let mut src_path: *const std::os::raw::c_char = std::ptr::null();
let err = subversion_sys::svn_fs_copied_from(
&mut rev,
&mut src_path,
self.ptr,
fs_path.as_ptr(),
result_pool.as_mut_ptr(),
);
svn_result(err)?;
if src_path.is_null() || rev < 0 {
return Ok(None);
}
let path_str = std::ffi::CStr::from_ptr(src_path)
.to_string_lossy()
.into_owned();
Ok(Some((Revnum(rev), path_str)))
}
}
pub fn node_relation(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
other_root: &Root,
other_path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<crate::NodeRelation, Error<'static>> {
let fs_path = path.try_into()?;
let other_fs_path = other_path.try_into()?;
with_tmp_pool(|pool| unsafe {
let mut relation: subversion_sys::svn_fs_node_relation_t =
subversion_sys::svn_fs_node_relation_t_svn_fs_node_unrelated;
let err = subversion_sys::svn_fs_node_relation(
&mut relation,
self.ptr,
fs_path.as_ptr(),
other_root.ptr,
other_fs_path.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(relation.into())
})
}
}
pub struct NodeHistory {
ptr: *mut subversion_sys::svn_fs_history_t,
pool: apr::Pool<'static>, }
impl NodeHistory {
pub fn prev(&mut self, cross_copies: bool) -> Result<Option<(String, Revnum)>, Error<'_>> {
unsafe {
let mut prev_history: *mut subversion_sys::svn_fs_history_t = std::ptr::null_mut();
let err = subversion_sys::svn_fs_history_prev2(
&mut prev_history,
self.ptr,
if cross_copies { 1 } else { 0 },
self.pool.as_mut_ptr(),
self.pool.as_mut_ptr(),
);
svn_result(err)?;
if prev_history.is_null() {
return Ok(None);
}
self.ptr = prev_history;
let mut path: *const std::ffi::c_char = std::ptr::null();
let mut rev: subversion_sys::svn_revnum_t = -1;
let err = subversion_sys::svn_fs_history_location(
&mut path,
&mut rev,
prev_history,
self.pool.as_mut_ptr(),
);
svn_result(err)?;
if path.is_null() {
return Ok(None);
}
let path_str = std::ffi::CStr::from_ptr(path)
.to_string_lossy()
.into_owned();
Ok(Some((path_str, Revnum(rev))))
}
}
}
impl Drop for NodeHistory {
fn drop(&mut self) {
}
}
pub struct NodeId {
ptr: *const subversion_sys::svn_fs_id_t,
pool: apr::Pool<'static>, }
impl PartialEq for NodeId {
fn eq(&self, other: &Self) -> bool {
unsafe { subversion_sys::svn_fs_compare_ids(self.ptr, other.ptr) == 0 }
}
}
impl Eq for NodeId {}
impl NodeId {
#[deprecated(note = "svn_fs_parse_id is deprecated in the SVN C API and is not \
guaranteed to work with all filesystem types")]
pub fn parse(data: &[u8]) -> Result<NodeId, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let ptr = subversion_sys::svn_fs_parse_id(
data.as_ptr() as *const std::os::raw::c_char,
data.len(),
pool.as_mut_ptr(),
);
if ptr.is_null() {
return Err(Error::from_message("Failed to parse node ID"));
}
Ok(NodeId { ptr, pool })
}
}
pub fn compare(&self, other: &NodeId) -> i32 {
unsafe { subversion_sys::svn_fs_compare_ids(self.ptr, other.ptr) }
}
pub fn check_related(&self, other: &NodeId) -> bool {
unsafe { subversion_sys::svn_fs_check_related(self.ptr, other.ptr) != 0 }
}
pub fn to_string(&self) -> Result<String, Error<'static>> {
unsafe {
let str_svn = subversion_sys::svn_fs_unparse_id(self.ptr, self.pool.as_mut_ptr());
if str_svn.is_null() {
return Err(Error::from_message("Failed to unparse node ID"));
}
let str_ptr = (*str_svn).data;
let str_len = (*str_svn).len;
let bytes = std::slice::from_raw_parts(str_ptr as *const u8, str_len);
let result = String::from_utf8_lossy(bytes).into_owned();
Ok(result)
}
}
}
pub struct Transaction<'fs> {
ptr: *mut subversion_sys::svn_fs_txn_t,
#[allow(dead_code)]
pool: apr::Pool<'static>,
_marker: std::marker::PhantomData<(*mut (), &'fs ())>,
}
impl Drop for Transaction<'_> {
fn drop(&mut self) {
}
}
impl<'fs> Transaction<'fs> {
#[cfg(feature = "repos")]
pub(crate) unsafe fn from_ptr_and_pool(
ptr: *mut subversion_sys::svn_fs_txn_t,
pool: apr::Pool<'static>,
) -> Self {
Self {
ptr,
pool,
_marker: std::marker::PhantomData,
}
}
#[allow(dead_code)]
pub(crate) fn as_ptr(&self) -> *mut subversion_sys::svn_fs_txn_t {
self.ptr
}
pub fn name(&self) -> Result<String, Error<'static>> {
with_tmp_pool(|pool| unsafe {
let mut name_ptr = std::ptr::null();
let err = subversion_sys::svn_fs_txn_name(&mut name_ptr, self.ptr, pool.as_mut_ptr());
Error::from_raw(err)?;
let name_cstr = std::ffi::CStr::from_ptr(name_ptr);
Ok(name_cstr.to_str()?.to_string())
})
}
pub fn base_revision(&self) -> Result<Revnum, Error<'static>> {
unsafe {
let base_rev = subversion_sys::svn_fs_txn_base_revision(self.ptr);
Ok(Revnum(base_rev))
}
}
pub fn root(&mut self) -> Result<TxnRoot<'_>, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut root_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_txn_root(&mut root_ptr, self.ptr, pool.as_mut_ptr());
Error::from_raw(err)?;
Ok(TxnRoot {
ptr: root_ptr,
_pool: pool,
_marker: std::marker::PhantomData,
})
}
}
pub fn change_prop(&mut self, name: &str, value: &str) -> Result<(), Error<'static>> {
let name_cstr = std::ffi::CString::new(name)?;
let value_str = subversion_sys::svn_string_t {
data: value.as_ptr() as *mut _,
len: value.len(),
};
let pool = apr::Pool::new();
unsafe {
let err = subversion_sys::svn_fs_change_txn_prop(
self.ptr,
name_cstr.as_ptr(),
&value_str,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
}
}
pub fn commit(self) -> Result<Revnum, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut new_rev = 0;
let err = subversion_sys::svn_fs_commit_txn(
std::ptr::null_mut(), &mut new_rev,
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(Revnum(new_rev))
}
}
pub fn abort(self) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let err = subversion_sys::svn_fs_abort_txn(self.ptr, pool.as_mut_ptr());
Error::from_raw(err)?;
Ok(())
}
}
pub fn change_prop_bytes(
&self,
name: &str,
value: Option<&[u8]>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let name_cstr = std::ffi::CString::new(name)?;
let value_ptr = value
.map(|val| crate::svn_string_helpers::svn_string_ncreate(val, &pool))
.unwrap_or(std::ptr::null_mut());
unsafe {
let err = subversion_sys::svn_fs_change_txn_prop(
self.ptr,
name_cstr.as_ptr(),
value_ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
}
Ok(())
}
pub fn change_props(&self, props: &[(&str, Option<&[u8]>)]) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let cstrings: Vec<std::ffi::CString> = props
.iter()
.map(|(name, _)| std::ffi::CString::new(*name))
.collect::<Result<_, _>>()?;
let mut arr =
apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(&pool, props.len() as i32);
for (cstr, (_, value)) in cstrings.iter().zip(props.iter()) {
let value_ptr = value
.map(|val| crate::svn_string_helpers::svn_string_ncreate(val, &pool))
.unwrap_or(std::ptr::null_mut());
arr.push(subversion_sys::svn_prop_t {
name: cstr.as_ptr(),
value: value_ptr as *const subversion_sys::svn_string_t,
});
}
unsafe {
let err =
subversion_sys::svn_fs_change_txn_props(self.ptr, arr.as_ptr(), pool.as_mut_ptr());
Error::from_raw(err)?;
}
Ok(())
}
pub fn prop(&self, name: &str) -> Result<Option<Vec<u8>>, Error<'_>> {
let pool = apr::Pool::new();
let name_cstr = std::ffi::CString::new(name)?;
let mut value_ptr = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_fs_txn_prop(
&mut value_ptr,
self.ptr,
name_cstr.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
if value_ptr.is_null() {
Ok(None)
} else {
let svn_str = &*value_ptr;
let slice = std::slice::from_raw_parts(svn_str.data as *const u8, svn_str.len);
Ok(Some(slice.to_vec()))
}
}
}
pub fn proplist(&self) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
let pool = apr::Pool::new();
let mut props_ptr = std::ptr::null_mut();
unsafe {
let err =
subversion_sys::svn_fs_txn_proplist(&mut props_ptr, self.ptr, pool.as_mut_ptr());
Error::from_raw(err)?;
let mut props = std::collections::HashMap::new();
if !props_ptr.is_null() {
let prop_hash = crate::props::PropHash::from_ptr(props_ptr);
props = prop_hash.to_hashmap();
}
Ok(props)
}
}
pub fn repos_change_prop(
&self,
name: &str,
value: Option<&[u8]>,
) -> Result<(), Error<'static>> {
let name_cstr = std::ffi::CString::new(name)?;
let pool = apr::Pool::new();
let value_ptr = value
.map(|val| crate::svn_string_helpers::svn_string_ncreate(val, &pool))
.unwrap_or(std::ptr::null_mut());
unsafe {
let err = subversion_sys::svn_repos_fs_change_txn_prop(
self.ptr,
name_cstr.as_ptr(),
value_ptr as *const subversion_sys::svn_string_t,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
}
Ok(())
}
pub fn repos_change_props(
&self,
props: &[(&str, Option<&[u8]>)],
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let cstrings: Vec<std::ffi::CString> = props
.iter()
.map(|(name, _)| std::ffi::CString::new(*name))
.collect::<Result<_, _>>()?;
let mut arr =
apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(&pool, props.len() as i32);
for (cstr, (_, value)) in cstrings.iter().zip(props.iter()) {
let value_ptr = value
.map(|val| crate::svn_string_helpers::svn_string_ncreate(val, &pool))
.unwrap_or(std::ptr::null_mut());
arr.push(subversion_sys::svn_prop_t {
name: cstr.as_ptr(),
value: value_ptr as *const subversion_sys::svn_string_t,
});
}
unsafe {
let err = subversion_sys::svn_repos_fs_change_txn_props(
self.ptr,
arr.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
}
Ok(())
}
}
pub struct TxnRoot<'txn> {
ptr: *mut subversion_sys::svn_fs_root_t,
_pool: apr::Pool<'static>,
_marker: std::marker::PhantomData<(*mut (), &'txn mut ())>,
}
impl<'txn> TxnRoot<'txn> {
pub fn make_dir(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let err =
subversion_sys::svn_fs_make_dir(self.ptr, fs_path.as_ptr(), pool.as_mut_ptr());
Error::from_raw(err)?;
Ok(())
}
}
pub fn make_file(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let err =
subversion_sys::svn_fs_make_file(self.ptr, fs_path.as_ptr(), pool.as_mut_ptr());
Error::from_raw(err)?;
Ok(())
}
}
pub fn delete(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let err = subversion_sys::svn_fs_delete(self.ptr, fs_path.as_ptr(), pool.as_mut_ptr());
Error::from_raw(err)?;
Ok(())
}
}
pub fn copy(
&mut self,
from_root: &Root,
from_path: impl TryInto<FsPath, Error = Error<'static>>,
to_path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<(), Error<'static>> {
let from_fs_path = from_path.try_into()?;
let to_fs_path = to_path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let err = subversion_sys::svn_fs_copy(
from_root.ptr,
from_fs_path.as_ptr(),
self.ptr,
to_fs_path.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
}
}
pub fn apply_text(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
result_checksum: Option<&str>,
) -> Result<crate::io::Stream, Error<'static>> {
let fs_path = path.try_into()?;
let checksum_cstr = result_checksum.map(std::ffi::CString::new).transpose()?;
let pool = apr::Pool::new();
unsafe {
let mut stream_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_apply_text(
&mut stream_ptr,
self.ptr,
fs_path.as_ptr(),
checksum_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr() as *const _),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(crate::io::Stream::from_ptr(stream_ptr, pool))
}
}
pub fn change_node_prop(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
name: &str,
value: &[u8],
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
let name_cstr = std::ffi::CString::new(name)?;
let value_str = if value.is_empty() {
std::ptr::null()
} else {
&subversion_sys::svn_string_t {
data: value.as_ptr() as *mut _,
len: value.len(),
}
};
let pool = apr::Pool::new();
unsafe {
let err = subversion_sys::svn_fs_change_node_prop(
self.ptr,
fs_path.as_ptr(),
name_cstr.as_ptr(),
value_str,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
}
}
pub fn check_path(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<crate::NodeKind, Error<'static>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut kind = 0;
let err = subversion_sys::svn_fs_check_path(
&mut kind,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(crate::NodeKind::from(kind))
}
}
pub fn set_file_contents(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
contents: &[u8],
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut stream = std::ptr::null_mut();
let err = subversion_sys::svn_fs_apply_text(
&mut stream,
self.ptr,
fs_path.as_ptr(),
std::ptr::null(), pool.as_mut_ptr(),
);
Error::from_raw(err)?;
let bytes_written = subversion_sys::svn_stream_write(
stream,
contents.as_ptr() as *const std::ffi::c_char,
&mut { contents.len() },
);
Error::from_raw(bytes_written)?;
let err = subversion_sys::svn_stream_close(stream);
Error::from_raw(err)?;
}
Ok(())
}
pub fn file_contents(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<Vec<u8>, Error<'_>> {
let fs_path = path.try_into()?;
let pool = apr::Pool::new();
unsafe {
let mut stream = std::ptr::null_mut();
let err = subversion_sys::svn_fs_file_contents(
&mut stream,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
let mut contents = Vec::new();
let mut buffer = [0u8; 4096];
loop {
let mut len = buffer.len();
let err = subversion_sys::svn_stream_read2(
stream,
buffer.as_mut_ptr() as *mut std::ffi::c_char,
&mut len,
);
Error::from_raw(err)?;
if len == 0 {
break;
}
contents.extend_from_slice(&buffer[..len]);
}
let err = subversion_sys::svn_stream_close(stream);
Error::from_raw(err)?;
Ok(contents)
}
}
pub fn txn_root_name(&self) -> String {
with_tmp_pool(|pool| unsafe {
let name = subversion_sys::svn_fs_txn_root_name(self.ptr, pool.as_mut_ptr());
std::ffi::CStr::from_ptr(name)
.to_string_lossy()
.into_owned()
})
}
pub fn txn_root_base_revision(&self) -> Revnum {
Revnum(unsafe { subversion_sys::svn_fs_txn_root_base_revision(self.ptr) })
}
pub fn revision_link(
&mut self,
from_root: &Root,
path: impl TryInto<FsPath, Error = Error<'static>>,
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_fs_revision_link(
from_root.ptr,
self.ptr,
fs_path.as_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn repos_change_node_prop(
&mut self,
path: impl TryInto<FsPath, Error = Error<'static>>,
name: &str,
value: Option<&[u8]>,
) -> Result<(), Error<'static>> {
let fs_path = path.try_into()?;
let name_cstr = std::ffi::CString::new(name)?;
let pool = apr::Pool::new();
let value_ptr = value
.map(|val| crate::svn_string_helpers::svn_string_ncreate(val, &pool))
.unwrap_or(std::ptr::null_mut());
unsafe {
let err = subversion_sys::svn_repos_fs_change_node_prop(
self.ptr,
fs_path.as_ptr(),
name_cstr.as_ptr(),
value_ptr as *const subversion_sys::svn_string_t,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
}
Ok(())
}
pub fn repos_get_inherited_props(
&self,
path: impl TryInto<FsPath, Error = Error<'static>>,
propname: Option<&str>,
) -> Result<Vec<(String, std::collections::HashMap<String, Vec<u8>>)>, Error<'static>> {
let fs_path = path.try_into()?;
let propname_cstr = propname.map(std::ffi::CString::new).transpose()?;
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let mut inherited_props: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_repos_fs_get_inherited_props(
&mut inherited_props,
self.ptr,
fs_path.as_ptr(),
propname_cstr
.as_ref()
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null()),
None,
std::ptr::null_mut(),
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
Error::from_raw(err)?;
}
if inherited_props.is_null() {
return Ok(Vec::new());
}
parse_inherited_props_array(inherited_props)
}
}
pub(crate) fn parse_inherited_props_array(
array: *mut apr_sys::apr_array_header_t,
) -> Result<Vec<(String, std::collections::HashMap<String, Vec<u8>>)>, Error<'static>> {
use std::collections::HashMap;
if array.is_null() {
return Ok(Vec::new());
}
let slice = unsafe {
std::slice::from_raw_parts(
(*array).elts as *const *const subversion_sys::svn_prop_inherited_item_t,
(*array).nelts as usize,
)
};
let mut result = Vec::new();
for item_ptr in slice.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()
.into_owned()
};
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)
}
impl<'pool> Fs<'pool> {
pub fn begin_txn(
&self,
base_rev: Revnum,
flags: u32,
) -> Result<Transaction<'_>, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut txn_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_begin_txn2(
&mut txn_ptr,
self.as_ptr() as *mut _,
base_rev.into(),
flags,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(Transaction {
ptr: txn_ptr,
pool,
_marker: std::marker::PhantomData,
})
}
}
pub fn open_txn(&self, name: &str) -> Result<Transaction<'_>, Error<'static>> {
let name_cstr = std::ffi::CString::new(name)?;
let pool = apr::Pool::new();
unsafe {
let mut txn_ptr = std::ptr::null_mut();
let err = subversion_sys::svn_fs_open_txn(
&mut txn_ptr,
self.as_ptr() as *mut _,
name_cstr.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(Transaction {
ptr: txn_ptr,
pool,
_marker: std::marker::PhantomData,
})
}
}
pub fn list_transactions(&self) -> Result<Vec<String>, Error<'_>> {
let pool = apr::Pool::new();
unsafe {
let mut names_array = std::ptr::null_mut();
let err = subversion_sys::svn_fs_list_transactions(
&mut names_array,
self.as_ptr() as *mut _,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
if names_array.is_null() {
return Ok(Vec::new());
}
let array = &*names_array;
let mut result = Vec::new();
for i in 0..array.nelts {
let name_ptr = *((array.elts as *const *const std::ffi::c_char).add(i as usize));
if !name_ptr.is_null() {
let name = std::ffi::CStr::from_ptr(name_ptr).to_str()?.to_string();
result.push(name);
}
}
Ok(result)
}
}
pub fn purge_txn(&self, name: &str) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let name_cstr = std::ffi::CString::new(name)?;
unsafe {
let err = subversion_sys::svn_fs_purge_txn(
self.as_ptr() as *mut _,
name_cstr.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn merge(
&self,
source: &Root<'_>,
target: &mut TxnRoot<'_>,
ancestor: &Root<'_>,
ancestor_path: &str,
source_path: &str,
target_path: &str,
) -> Result<Option<String>, Error<'_>> {
let pool = apr::Pool::new();
let ancestor_path_cstr = std::ffi::CString::new(ancestor_path)?;
let source_path_cstr = std::ffi::CString::new(source_path)?;
let target_path_cstr = std::ffi::CString::new(target_path)?;
unsafe {
let mut conflict_ptr = std::ptr::null();
let err = subversion_sys::svn_fs_merge(
&mut conflict_ptr,
source.ptr,
source_path_cstr.as_ptr(),
target.ptr,
target_path_cstr.as_ptr(),
ancestor.ptr,
ancestor_path_cstr.as_ptr(),
pool.as_mut_ptr(),
);
if !err.is_null() {
let err_code = (*err).apr_err;
if err_code == subversion_sys::svn_errno_t_SVN_ERR_FS_CONFLICT as i32 {
let conflict = if !conflict_ptr.is_null() {
Some(std::ffi::CStr::from_ptr(conflict_ptr).to_str()?.to_string())
} else {
None
};
subversion_sys::svn_error_clear(err);
return Ok(conflict);
}
Error::from_raw(err)?;
}
Ok(None)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use tempfile::tempdir;
#[test]
fn test_fs_create_and_open() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
Fs::open(&fs_path).unwrap();
}
#[test]
fn test_fs_youngest_rev() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let rev = fs.youngest_revision().unwrap();
assert_eq!(rev, crate::Revnum(0));
}
#[test]
fn test_fs_get_uuid() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let uuid = fs.get_uuid().unwrap();
assert!(!uuid.is_empty());
}
#[test]
fn test_fs_mutability() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
fs.get_uuid().unwrap();
fs.revision_proplist(crate::Revnum(0), false).unwrap();
fs.revision_root(crate::Revnum(0)).unwrap();
fs.youngest_revision().unwrap();
}
#[test]
fn test_root_creation() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
assert!(root.is_revision_root());
}
#[test]
fn test_root_fs_accessor() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let fs2 = root.fs();
assert_eq!(fs.get_uuid().unwrap(), fs2.get_uuid().unwrap());
}
#[test]
fn test_drop_cleanup() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
{
let _fs = Fs::create(&fs_path).unwrap();
}
let fs = Fs::open(&fs_path).unwrap();
fs.youngest_revision().unwrap();
}
#[test]
fn test_root_check_path() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let kind = root.check_path("/").unwrap();
assert_eq!(kind, crate::NodeKind::Dir);
let kind = root.check_path("/nonexistent").unwrap();
assert_eq!(kind, crate::NodeKind::None);
}
#[test]
fn test_root_is_dir() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
assert!(root.is_dir("/").unwrap());
assert!(!root.is_dir("/nonexistent").unwrap_or(false));
}
#[test]
fn test_root_dir_entries() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let entries = root.dir_entries("/").unwrap();
assert!(entries.is_empty());
}
#[test]
fn test_dir_entries_optimal_order() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
{
let mut root = txn.root().unwrap();
root.make_file("/alpha.txt").unwrap();
root.make_file("/beta.txt").unwrap();
root.make_file("/gamma.txt").unwrap();
}
let _rev = txn.commit().unwrap();
let root = fs.revision_root(crate::Revnum(1)).unwrap();
let ordered = root.dir_entries_optimal_order("/").unwrap();
assert_eq!(ordered.len(), 3);
let mut names: Vec<String> = ordered.iter().map(|e| e.name().to_owned()).collect();
names.sort();
assert_eq!(names, vec!["alpha.txt", "beta.txt", "gamma.txt"]);
}
#[test]
#[allow(deprecated)]
#[cfg_attr(windows, ignore = "svn_fs_parse_id may not work with all FS backends")]
fn test_node_id_parse_roundtrip() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let node_id = root.node_id("/").unwrap();
let id_string = node_id.to_string().unwrap();
let parsed = match NodeId::parse(id_string.as_bytes()) {
Ok(p) => p,
Err(e) if e.message() == Some("Failed to parse node ID") => return,
Err(e) => panic!("Unexpected error: {}", e),
};
let reparsed_string = parsed.to_string().unwrap();
assert_eq!(id_string, reparsed_string);
}
#[test]
fn test_root_proplist() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let props = root.proplist("/").unwrap();
assert!(props.is_empty());
}
#[test]
fn test_root_paths_changed() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let changes = root.paths_changed().unwrap();
assert!(changes.is_empty());
}
#[test]
fn test_fs_path_change_accessors() {
}
#[test]
fn test_fs_dir_entry_accessors() {
}
#[test]
fn test_transaction_basic() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let name = txn.name().unwrap();
assert!(!name.is_empty(), "Transaction should have a name");
let base_rev = txn.base_revision().unwrap();
assert_eq!(
base_rev,
crate::Revnum::from(0u32),
"Base revision should be 0"
);
txn.change_prop("svn:log", "Test commit message").unwrap();
txn.change_prop("svn:author", "test-user").unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
let kind = root.check_path("/trunk").unwrap();
assert_eq!(kind, crate::NodeKind::Dir);
root.make_file("/trunk/test.txt").unwrap();
let kind = root.check_path("/trunk/test.txt").unwrap();
assert_eq!(kind, crate::NodeKind::File);
let mut stream = root.apply_text("/trunk/test.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Hello, World!\n").unwrap();
drop(stream);
root.change_node_prop("/trunk/test.txt", "custom:prop", b"value")
.unwrap();
let new_rev = txn.commit().unwrap();
assert_eq!(
new_rev,
crate::Revnum::from(1u32),
"First commit should be revision 1"
);
let youngest = fs.youngest_revision().unwrap();
assert_eq!(youngest, crate::Revnum::from(1u32));
}
#[test]
fn test_transaction_abort() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/test-dir").unwrap();
root.make_file("/test-file.txt").unwrap();
txn.abort().unwrap();
let youngest = fs.youngest_revision().unwrap();
assert_eq!(
youngest,
crate::Revnum(0),
"No changes should be committed after abort"
);
}
#[test]
fn test_transaction_copy() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn1 = fs.begin_txn(crate::Revnum(0), 0).unwrap();
txn1.change_prop("svn:log", "Initial commit").unwrap();
let mut root1 = txn1.root().unwrap();
root1.make_dir("/original").unwrap();
root1.make_file("/original/file.txt").unwrap();
let mut stream = root1.apply_text("/original/file.txt", None).unwrap();
use std::io::Write;
stream.write_all(b"Original content\n").unwrap();
drop(stream);
let rev1 = txn1.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
txn2.change_prop("svn:log", "Copy operation").unwrap();
let mut root2 = txn2.root().unwrap();
let rev1_root = fs.revision_root(rev1).unwrap();
root2.copy(&rev1_root, "/original", "/copy").unwrap();
let kind = root2.check_path("/copy").unwrap();
assert_eq!(kind, crate::NodeKind::Dir);
let kind = root2.check_path("/copy/file.txt").unwrap();
assert_eq!(kind, crate::NodeKind::File);
let _rev2 = txn2.commit().unwrap();
}
#[test]
fn test_transaction_delete() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn1 = fs.begin_txn(crate::Revnum(0), 0).unwrap();
txn1.change_prop("svn:log", "Create files").unwrap();
let mut root1 = txn1.root().unwrap();
root1.make_dir("/dir1").unwrap();
root1.make_file("/dir1/file1.txt").unwrap();
root1.make_file("/file2.txt").unwrap();
let rev1 = txn1.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
txn2.change_prop("svn:log", "Delete files").unwrap();
let mut root2 = txn2.root().unwrap();
root2.delete("/file2.txt").unwrap();
let kind = root2.check_path("/file2.txt").unwrap();
assert_eq!(kind, crate::NodeKind::None);
let kind = root2.check_path("/dir1/file1.txt").unwrap();
assert_eq!(kind, crate::NodeKind::File);
let _rev2 = txn2.commit().unwrap();
}
#[test]
fn test_transaction_properties() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
root.change_node_prop("/test.txt", "svn:mime-type", b"text/plain")
.unwrap();
root.change_node_prop("/test.txt", "custom:author", b"test-user")
.unwrap();
root.change_node_prop("/test.txt", "custom:description", b"A test file")
.unwrap();
root.change_node_prop("/test.txt", "custom:empty", b"")
.unwrap();
txn.change_prop("svn:log", "Test commit with properties")
.unwrap();
txn.change_prop("svn:author", "property-tester").unwrap();
txn.change_prop("svn:date", "2023-01-01T12:00:00.000000Z")
.unwrap();
let _rev = txn.commit().unwrap();
}
#[test]
fn test_open_transaction() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let txn_name = txn.name().unwrap();
drop(txn);
let _result = fs.open_txn(&txn_name);
}
#[test]
fn test_node_history_and_content_comparison() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/test.txt").unwrap();
let mut stream = txn_root.apply_text("/test.txt", None).unwrap();
use std::io::Write;
write!(stream, "Initial content").unwrap();
stream.close().unwrap();
let rev1 = txn.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
let mut txn_root2 = txn2.root().unwrap();
let mut stream2 = txn_root2.apply_text("/test.txt", None).unwrap();
write!(stream2, "Modified content").unwrap();
stream2.close().unwrap();
let rev2 = txn2.commit().unwrap();
let root1 = fs.revision_root(rev1).unwrap();
let root2 = fs.revision_root(rev2).unwrap();
let contents_changed = root1
.contents_changed("/test.txt", &root2, "/test.txt")
.unwrap();
assert!(
contents_changed,
"File contents should have changed between revisions"
);
let contents_same = root1
.contents_changed("/test.txt", &root1, "/test.txt")
.unwrap();
assert!(
!contents_same,
"File contents should be the same in the same revision"
);
let _history = root2.node_history("/test.txt").unwrap();
let created_rev1 = root1.node_created_rev("/test.txt").unwrap();
assert_eq!(
created_rev1, rev1,
"Node in rev1 should have been created in rev1"
);
let created_rev2 = root2.node_created_rev("/test.txt").unwrap();
assert_eq!(
created_rev2, rev2,
"Node in rev2 should have been created in rev2 (after modification)"
);
let node_id1 = root1.node_id("/test.txt").unwrap();
let node_id2 = root2.node_id("/test.txt").unwrap();
let _id1_str = node_id1.to_string().unwrap();
let _id2_str = node_id2.to_string().unwrap();
}
#[test]
fn test_props_changed() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/test.txt").unwrap();
txn_root
.change_node_prop("/test.txt", "custom:prop", b"value1")
.unwrap();
let rev1 = txn.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
let mut txn_root2 = txn2.root().unwrap();
txn_root2
.change_node_prop("/test.txt", "custom:prop", b"value2")
.unwrap();
let rev2 = txn2.commit().unwrap();
let root1 = fs.revision_root(rev1).unwrap();
let root2 = fs.revision_root(rev2).unwrap();
let props_changed = root1
.props_changed("/test.txt", &root2, "/test.txt")
.unwrap();
assert!(
props_changed,
"Properties should have changed between revisions"
);
let props_same = root1
.props_changed("/test.txt", &root1, "/test.txt")
.unwrap();
assert!(
!props_same,
"Properties should be the same in the same revision"
);
}
#[test]
fn test_transaction_operations() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let txns = fs.list_transactions().unwrap();
assert_eq!(txns.len(), 0, "Should have no transactions initially");
let txn = fs.begin_txn(Revnum(0), 0).unwrap();
let txn_name = txn.name().unwrap();
let txns = fs.list_transactions().unwrap();
assert_eq!(txns.len(), 1, "Should have one transaction");
assert_eq!(txns[0], txn_name);
txn.abort().unwrap();
let txns = fs.list_transactions().unwrap();
assert_eq!(txns.len(), 0, "Should have no transactions after abort");
}
#[test]
fn test_txn_file_operations() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn1 = fs.begin_txn(Revnum(0), 0).unwrap();
let mut txn_root1 = txn1.root().unwrap();
txn_root1.make_file("/test.txt").unwrap();
txn_root1
.set_file_contents("/test.txt", b"Hello, World!")
.unwrap();
let rev1 = txn1.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
let mut txn_root2 = txn2.root().unwrap();
let base_root = fs.revision_root(rev1).unwrap();
txn_root2
.copy(&base_root, "/test.txt", "/renamed.txt")
.unwrap();
txn_root2.delete("/test.txt").unwrap();
let old_kind = txn_root2.check_path("/test.txt").unwrap();
assert_eq!(old_kind, crate::NodeKind::None);
let new_kind = txn_root2.check_path("/renamed.txt").unwrap();
assert_eq!(new_kind, crate::NodeKind::File);
let rev2 = txn2.commit().unwrap();
let root = fs.revision_root(rev2).unwrap();
let mut stream = root.file_contents("/renamed.txt").unwrap();
let mut contents = Vec::new();
let mut buffer = [0u8; 1024];
loop {
let bytes_read = stream.read_full(&mut buffer).unwrap();
if bytes_read == 0 {
break;
}
contents.extend_from_slice(&buffer[..bytes_read]);
}
assert_eq!(contents, b"Hello, World!");
}
#[test]
fn test_merge_trees() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn1 = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root1 = txn1.root().unwrap();
root1.make_file("/file.txt").unwrap();
root1
.set_file_contents("/file.txt", b"Initial content")
.unwrap();
let rev1 = txn1.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
let mut root2 = txn2.root().unwrap();
root2
.set_file_contents("/file.txt", b"Branch 1 content")
.unwrap();
let rev2 = txn2.commit().unwrap();
let mut txn3 = fs.begin_txn(rev1, 0).unwrap();
let mut root3 = txn3.root().unwrap();
root3
.set_file_contents("/file.txt", b"Branch 2 content")
.unwrap();
let ancestor = fs.revision_root(rev1).unwrap();
let source = fs.revision_root(rev2).unwrap();
let conflict = fs
.merge(&source, &mut root3, &ancestor, "", "", "")
.unwrap();
assert!(conflict.is_some(), "Should have a merge conflict");
}
#[test]
fn test_contents_and_props_changed() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn1 = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root1 = txn1.root().unwrap();
root1.make_file("/file.txt").unwrap();
root1
.set_file_contents("/file.txt", b"Original content")
.unwrap();
root1
.change_node_prop("/file.txt", "custom:prop", b"value1")
.unwrap();
let rev1 = txn1.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
let mut root2 = txn2.root().unwrap();
root2
.set_file_contents("/file.txt", b"Modified content")
.unwrap();
let rev2 = txn2.commit().unwrap();
let root_r1 = fs.revision_root(rev1).unwrap();
let root_r2 = fs.revision_root(rev2).unwrap();
let contents_changed = root_r1
.contents_changed("/file.txt", &root_r2, "/file.txt")
.unwrap();
assert!(contents_changed, "Contents should have changed");
let props_changed = root_r1
.props_changed("/file.txt", &root_r2, "/file.txt")
.unwrap();
assert!(!props_changed, "Properties should not have changed");
let mut txn3 = fs.begin_txn(rev2, 0).unwrap();
let mut root3 = txn3.root().unwrap();
root3
.change_node_prop("/file.txt", "custom:prop", b"value2")
.unwrap();
let rev3 = txn3.commit().unwrap();
let root_r3 = fs.revision_root(rev3).unwrap();
let contents_changed = root_r2
.contents_changed("/file.txt", &root_r3, "/file.txt")
.unwrap();
assert!(!contents_changed, "Contents should not have changed");
let props_changed = root_r2
.props_changed("/file.txt", &root_r3, "/file.txt")
.unwrap();
assert!(props_changed, "Properties should have changed");
}
#[test]
fn test_node_history() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn1 = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root1 = txn1.root().unwrap();
root1.make_file("/file.txt").unwrap();
root1.set_file_contents("/file.txt", b"Version 1").unwrap();
let rev1 = txn1.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
let mut root2 = txn2.root().unwrap();
root2.set_file_contents("/file.txt", b"Version 2").unwrap();
let rev2 = txn2.commit().unwrap();
let mut txn3 = fs.begin_txn(rev2, 0).unwrap();
let mut root3 = txn3.root().unwrap();
let source_root = fs.revision_root(rev2).unwrap();
root3
.copy(&source_root, "/file.txt", "/copied.txt")
.unwrap();
let rev3 = txn3.commit().unwrap();
let root_r3 = fs.revision_root(rev3).unwrap();
let mut history = root_r3.node_history("/copied.txt").unwrap();
if let Some((path, revision)) = history.prev(true).unwrap() {
assert!(path.contains("copied.txt") || path.contains("file.txt"));
assert!(revision.0 <= rev3.0);
if let Some((prev_path, prev_revision)) = history.prev(true).unwrap() {
assert!(prev_path.contains("file.txt") || prev_path.contains("copied.txt"));
assert!(prev_revision.0 <= rev2.0);
}
}
}
#[test]
fn test_fs_lock_unlock() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("testuser").unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
root.set_file_contents("/test.txt", b"test content")
.unwrap();
let rev = txn.commit().unwrap();
let lock = fs.lock(
"/test.txt",
None, Some("Test lock comment"),
false,
None, rev,
false, );
let lock = lock.unwrap();
assert_eq!(lock.path(), "/test.txt");
assert!(!lock.token().is_empty());
assert_eq!(lock.comment(), "Test lock comment");
let lock_info = fs.get_lock("/test.txt").unwrap();
assert!(lock_info.is_some());
let lock_info = lock_info.unwrap();
assert_eq!(lock_info.path(), "/test.txt");
assert_eq!(lock_info.token(), lock.token());
fs.unlock("/test.txt", lock.token(), false).unwrap();
let lock_info = fs.get_lock("/test.txt").unwrap();
assert!(lock_info.is_none());
}
#[test]
fn test_fs_lock_steal() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("testuser").unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/locked.txt").unwrap();
root.set_file_contents("/locked.txt", b"content").unwrap();
let rev = txn.commit().unwrap();
let _lock1 = fs
.lock(
"/locked.txt",
None,
Some("First lock"),
false,
None,
rev,
false,
)
.unwrap();
{
let lock2 = fs.lock(
"/locked.txt",
None,
Some("Second lock"),
false,
None,
rev,
false, );
assert!(
lock2.is_err(),
"Should not be able to lock already locked file"
);
}
let lock3 = fs.lock(
"/locked.txt",
None,
Some("Stolen lock"),
false,
None,
rev,
true, );
let lock3 = lock3.unwrap();
assert_eq!(lock3.comment(), "Stolen lock");
}
#[test]
fn test_fs_get_locks() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("testuser").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_file("/dir1/file1.txt").unwrap();
root.make_file("/dir1/file2.txt").unwrap();
root.make_dir("/dir1/subdir").unwrap();
root.make_file("/dir1/subdir/file3.txt").unwrap();
let rev = txn.commit().unwrap();
fs.lock(
"/dir1/file1.txt",
None,
Some("Lock 1"),
false,
None,
rev,
false,
)
.unwrap();
fs.lock(
"/dir1/file2.txt",
None,
Some("Lock 2"),
false,
None,
rev,
false,
)
.unwrap();
fs.lock(
"/dir1/subdir/file3.txt",
None,
Some("Lock 3"),
false,
None,
rev,
false,
)
.unwrap();
let locks = fs.get_locks("/dir1", crate::Depth::Infinity).unwrap();
assert_eq!(locks.len(), 3, "Should find 3 locks");
let locks_immediate = fs.get_locks("/dir1", crate::Depth::Immediates).unwrap();
assert_eq!(
locks_immediate.len(),
2,
"Should find 2 locks at immediate depth"
);
let locks_files = fs.get_locks("/dir1", crate::Depth::Files).unwrap();
assert_eq!(locks_files.len(), 2, "Should find 2 locks with files depth");
}
#[test]
fn test_fs_generate_lock_token() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let token1 = fs.generate_lock_token().unwrap();
assert!(!token1.is_empty(), "Generated token should not be empty");
let token2 = fs.generate_lock_token().unwrap();
assert_ne!(token1, token2, "Generated tokens should be unique");
}
#[test]
fn test_get_access_username_no_access() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let result = fs.get_access_username().unwrap();
assert_eq!(result, None);
}
#[test]
fn test_get_access_username_with_access() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("alice").unwrap();
let result = fs.get_access_username().unwrap();
assert_eq!(result, Some("alice".to_string()));
}
#[test]
fn test_fs_access_add_lock_token() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("testuser").unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/locked.txt").unwrap();
let rev = txn.commit().unwrap();
let lock = fs
.lock(
"/locked.txt",
None,
Some("Test lock"),
false,
None,
rev,
false,
)
.unwrap();
fs.access_add_lock_token("/locked.txt", lock.token())
.unwrap();
}
#[test]
fn test_fs_access_add_lock_token_no_context() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
let result = fs.access_add_lock_token("/some/path", "some-token");
assert!(result.is_err(), "Should fail when no access context is set");
}
#[test]
fn test_fs_lock_many() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("testuser").unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file1.txt").unwrap();
root.make_file("/file2.txt").unwrap();
let rev = txn.commit().unwrap();
let targets = vec![("/file1.txt", None, rev), ("/file2.txt", None, rev)];
let mut results: Vec<(String, bool)> = Vec::new();
fs.lock_many(
&targets,
Some("bulk lock"),
false,
None,
false,
|path, error| {
results.push((path.to_string(), error.is_none()));
},
)
.unwrap();
assert_eq!(results.len(), 2);
assert!(
results.iter().all(|(_, ok)| *ok),
"All locks should succeed"
);
let lock1 = fs.get_lock("/file1.txt").unwrap();
assert!(lock1.is_some(), "file1.txt should be locked");
let lock2 = fs.get_lock("/file2.txt").unwrap();
assert!(lock2.is_some(), "file2.txt should be locked");
}
#[test]
fn test_fs_unlock_many() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.set_access("testuser").unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
let rev = txn.commit().unwrap();
let lock = fs
.lock("/file.txt", None, None, false, None, rev, false)
.unwrap();
let targets = vec![("/file.txt", lock.token())];
let mut results: Vec<(String, bool)> = Vec::new();
fs.unlock_many(&targets, false, |path, error| {
results.push((path.to_string(), error.is_none()));
})
.unwrap();
assert_eq!(results.len(), 1);
assert!(results[0].1, "unlock should succeed");
let lock_info = fs.get_lock("/file.txt").unwrap();
assert!(lock_info.is_none(), "file.txt should no longer be locked");
}
#[test]
fn test_fs_pack() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let result = pack(
&fs_path, None, None, );
assert!(result.is_ok(), "Pack should succeed on new repository");
}
#[test]
fn test_fs_verify() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let result = verify(
&fs_path, None, None, None, None, );
assert!(result.is_ok(), "Verify should succeed on valid repository");
}
#[test]
fn test_fs_hotcopy() {
let src_dir = tempdir().unwrap();
let src_path = src_dir.path().join("src-fs");
let dst_dir = tempdir().unwrap();
let dst_path = dst_dir.path().join("backup");
Fs::create(&src_path).unwrap();
let result = hotcopy(
&src_path, &dst_path, false, false, None, None, );
assert!(result.is_ok(), "Hotcopy should succeed");
let dst_fs = Fs::open(&dst_path);
assert!(
dst_fs.is_ok(),
"Hotcopy destination should be valid filesystem"
);
}
#[test]
fn test_fs_recover() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let result = recover(&fs_path, None);
assert!(result.is_ok(), "Recover should succeed");
}
#[test]
fn test_fs_freeze() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let mut fs = Fs::open(&fs_path).unwrap();
let mut callback_called = false;
let result = fs.freeze(|| {
callback_called = true;
Ok(())
});
assert!(result.is_ok(), "Freeze should succeed");
assert!(callback_called, "Freeze callback should be called");
}
#[test]
fn test_fs_freeze_with_error() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let mut fs = Fs::open(&fs_path).unwrap();
let result = fs.freeze(|| Err(Error::from_message("Test error from freeze callback")));
assert!(result.is_err(), "Freeze should propagate callback error");
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("Test error from freeze callback"));
}
#[test]
fn test_fs_info() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let fs = Fs::open(&fs_path).unwrap();
let result = fs.info();
assert!(result.is_ok(), "Info should succeed");
let info = result.unwrap();
if let Some(fs_type) = &info.fs_type {
assert!(
fs_type == "fsfs" || fs_type == "bdb" || fs_type == "fsx",
"Filesystem type should be a known type"
);
}
}
#[test]
fn test_pack_with_callbacks() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let result = pack(
&fs_path,
Some(Box::new(|_msg| {
})),
None,
);
assert!(result.is_ok(), "Pack with notify should succeed");
}
#[test]
fn test_verify_with_callbacks() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let result = verify(
&fs_path,
None,
None,
None,
Some(Box::new(|| false)), );
assert!(result.is_ok(), "Verify with cancel callback should succeed");
let result = verify(
&fs_path,
None,
None,
None,
Some(Box::new(|| true)), );
assert!(
result.is_err() || result.is_ok(),
"Verify should either cancel or complete"
);
}
#[test]
fn test_hotcopy_incremental() {
let src_dir = tempdir().unwrap();
let src_path = src_dir.path().join("src-fs");
let dst_dir = tempdir().unwrap();
let dst_path = dst_dir.path().join("backup");
Fs::create(&src_path).unwrap();
hotcopy(&src_path, &dst_path, false, false, None, None).unwrap();
let result = hotcopy(
&src_path, &dst_path, true, false, None, None,
);
if result.is_err() {
let _err = result.unwrap_err();
}
}
#[test]
fn test_apply_text_with_checksum() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
let expected_checksum = "8ddd8be4b179a529afa5f2ffae4b9858";
let mut stream = root
.apply_text("/test.txt", Some(expected_checksum))
.unwrap();
use std::io::Write;
stream.write_all(b"Hello, World!\n").unwrap();
drop(stream);
txn.commit().unwrap();
}
#[test]
fn test_apply_text_with_wrong_checksum() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
let wrong_checksum = "00000000000000000000000000000000";
root.apply_text("/test.txt", Some(wrong_checksum)).unwrap();
}
#[test]
fn test_begin_txn_with_flags() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let txn1 = fs.begin_txn(Revnum(0), 0).unwrap();
assert!(!txn1.name().unwrap().is_empty());
drop(txn1);
let txn2 = fs.begin_txn(Revnum(0), 0x00000001).unwrap();
assert!(!txn2.name().unwrap().is_empty());
drop(txn2);
let txn3 = fs.begin_txn(Revnum(0), 0x00000002).unwrap();
assert!(!txn3.name().unwrap().is_empty());
drop(txn3);
}
#[test]
fn test_transaction_properties_extended() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let binary_data = b"\x00\x01\x02\x03\xFF";
txn.change_prop_bytes("custom:binary", Some(binary_data))
.unwrap();
let prop_value = txn.prop("custom:binary").unwrap();
assert_eq!(prop_value.as_deref(), Some(binary_data.as_ref()));
txn.change_prop("svn:log", "Test commit").unwrap();
txn.change_prop("svn:author", "test-user").unwrap();
let props = txn.proplist().unwrap();
assert!(props.contains_key("svn:log"));
assert!(props.contains_key("svn:author"));
assert!(props.contains_key("custom:binary"));
txn.change_prop_bytes("custom:binary", None).unwrap();
let prop_value = txn.prop("custom:binary").unwrap();
assert!(prop_value.is_none() || prop_value.as_deref() == Some(b"".as_ref()));
}
#[test]
fn test_transaction_change_props_bulk() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_props(&[
("svn:log", Some(b"bulk commit" as &[u8])),
("svn:author", Some(b"alice" as &[u8])),
("custom:tag", Some(b"v1.0" as &[u8])),
])
.unwrap();
assert_eq!(
txn.prop("svn:log").unwrap().as_deref(),
Some(b"bulk commit" as &[u8])
);
assert_eq!(
txn.prop("svn:author").unwrap().as_deref(),
Some(b"alice" as &[u8])
);
assert_eq!(
txn.prop("custom:tag").unwrap().as_deref(),
Some(b"v1.0" as &[u8])
);
txn.change_props(&[("custom:tag", None)]).unwrap();
assert_eq!(txn.prop("custom:tag").unwrap(), None);
}
#[test]
fn test_node_id_compare_and_related() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "Initial commit").unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_file("/trunk/file.txt").unwrap();
root.set_file_contents("/trunk/file.txt", b"initial content")
.unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "Branch").unwrap();
let mut root = txn.root().unwrap();
let rev1_root = fs.revision_root(Revnum(1)).unwrap();
root.copy(&rev1_root, "/trunk", "/branch").unwrap();
txn.commit().unwrap();
let root1 = fs.revision_root(Revnum(1)).unwrap();
let root2 = fs.revision_root(Revnum(2)).unwrap();
let trunk_id1 = root1.node_id("/trunk").unwrap();
let trunk_id2 = root2.node_id("/trunk").unwrap();
let branch_id = root2.node_id("/branch").unwrap();
assert_eq!(trunk_id1.compare(&trunk_id2), 0);
assert!(trunk_id2.check_related(&branch_id));
assert!(trunk_id1.eq(&trunk_id2));
}
#[test]
fn test_root_closest_copy() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "Initial").unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_file("/trunk/file.txt").unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "Branch").unwrap();
let mut root = txn.root().unwrap();
let rev1_root = fs.revision_root(Revnum(1)).unwrap();
root.copy(&rev1_root, "/trunk", "/branch").unwrap();
txn.commit().unwrap();
let root = fs.revision_root(Revnum(2)).unwrap();
let result = root.closest_copy("/branch").unwrap();
assert!(result.is_some());
let (_copy_root, copy_path) = result.unwrap();
assert_eq!(copy_path, "/branch");
let result = root.closest_copy("/trunk").unwrap();
assert!(result.is_none());
}
#[test]
fn test_root_contents_and_props_different() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "Rev 1").unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
root.set_file_contents("/file.txt", b"content v1").unwrap();
root.change_node_prop("/file.txt", "custom:prop", b"value1")
.unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "Rev 2").unwrap();
let mut root = txn.root().unwrap();
root.set_file_contents("/file.txt", b"content v2").unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(2), 0).unwrap();
txn.change_prop("svn:log", "Rev 3").unwrap();
let mut root = txn.root().unwrap();
root.change_node_prop("/file.txt", "custom:prop", b"value2")
.unwrap();
txn.commit().unwrap();
let root1 = fs.revision_root(Revnum(1)).unwrap();
let root2 = fs.revision_root(Revnum(2)).unwrap();
let root3 = fs.revision_root(Revnum(3)).unwrap();
assert!(root1
.contents_different("/file.txt", &root2, "/file.txt")
.unwrap());
assert!(!root2
.contents_different("/file.txt", &root3, "/file.txt")
.unwrap());
assert!(!root1
.props_different("/file.txt", &root2, "/file.txt")
.unwrap());
assert!(root2
.props_different("/file.txt", &root3, "/file.txt")
.unwrap());
}
#[test]
fn test_apply_text() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/test.txt").unwrap();
let mut stream = root.apply_text("/test.txt", None).unwrap();
stream.write(b"Hello, world!").unwrap();
stream.close().unwrap();
txn.commit().unwrap();
let root = fs.revision_root(Revnum(1)).unwrap();
let mut contents = Vec::new();
root.file_contents("/test.txt")
.unwrap()
.read_to_end(&mut contents)
.unwrap();
assert_eq!(contents, b"Hello, world!");
}
#[test]
#[cfg(feature = "delta")]
fn test_get_file_delta_stream() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
let mut stream = root.apply_text("/file.txt", None).unwrap();
stream.write(b"First version").unwrap();
stream.close().unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
let mut root = txn.root().unwrap();
let mut stream = root.apply_text("/file.txt", None).unwrap();
stream.write(b"Second version with more text").unwrap();
stream.close().unwrap();
txn.commit().unwrap();
let root1 = fs.revision_root(Revnum(1)).unwrap();
let root2 = fs.revision_root(Revnum(2)).unwrap();
root1
.get_file_delta_stream("/file.txt", &root2, "/file.txt")
.unwrap();
}
#[test]
fn test_node_has_props() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/no-props.txt").unwrap();
root.make_file("/with-props.txt").unwrap();
root.change_node_prop("/with-props.txt", "custom:key", b"value")
.unwrap();
txn.commit().unwrap();
let root = fs.revision_root(Revnum(1)).unwrap();
assert!(
!root.node_has_props("/no-props.txt").unwrap(),
"file with no properties should return false"
);
assert!(
root.node_has_props("/with-props.txt").unwrap(),
"file with a property should return true"
);
}
#[test]
fn test_node_origin_rev() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "Initial").unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "Modify").unwrap();
let mut root = txn.root().unwrap();
root.set_file_contents("/file.txt", b"updated").unwrap();
txn.commit().unwrap();
let root2 = fs.revision_root(Revnum(2)).unwrap();
let origin_rev = root2.node_origin_rev("/file.txt").unwrap();
assert_eq!(origin_rev, Revnum(1), "origin revision should be 1");
}
#[test]
fn test_copied_from() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "Initial").unwrap();
let mut root = txn.root().unwrap();
root.make_file("/original.txt").unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "Copy").unwrap();
let mut txn_root = txn.root().unwrap();
let rev1_root = fs.revision_root(Revnum(1)).unwrap();
txn_root
.copy(&rev1_root, "/original.txt", "/copy.txt")
.unwrap();
txn.commit().unwrap();
let root2 = fs.revision_root(Revnum(2)).unwrap();
let result = root2.copied_from("/copy.txt").unwrap();
assert!(result.is_some(), "copied file should have a copy source");
let (rev, src_path) = result.unwrap();
assert_eq!(rev, Revnum(1), "copy source revision should be 1");
assert_eq!(src_path, "/original.txt", "copy source path should match");
let result = root2.copied_from("/original.txt").unwrap();
assert!(
result.is_none(),
"non-copied file should return None from copied_from"
);
}
#[test]
fn test_node_relation() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "Initial").unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "Modify").unwrap();
let mut root = txn.root().unwrap();
root.set_file_contents("/file.txt", b"changed").unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(2), 0).unwrap();
txn.change_prop("svn:log", "Add other").unwrap();
let mut root = txn.root().unwrap();
root.make_file("/other.txt").unwrap();
txn.commit().unwrap();
let root1 = fs.revision_root(Revnum(1)).unwrap();
let root2 = fs.revision_root(Revnum(2)).unwrap();
let root3 = fs.revision_root(Revnum(3)).unwrap();
let rel = root1
.node_relation("/file.txt", &root1, "/file.txt")
.unwrap();
assert_eq!(rel, crate::NodeRelation::Unchanged);
let rel = root1
.node_relation("/file.txt", &root2, "/file.txt")
.unwrap();
assert_eq!(rel, crate::NodeRelation::CommonAncestor);
let rel = root1
.node_relation("/file.txt", &root3, "/other.txt")
.unwrap();
assert_eq!(rel, crate::NodeRelation::Unrelated);
}
#[test]
fn test_node_prop_returns_value() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/tagged.txt").unwrap();
root.change_node_prop("/tagged.txt", "test:tag", b"my-value")
.unwrap();
let rev1 = txn.commit().unwrap();
let rev_root = fs.revision_root(rev1).unwrap();
let val = rev_root.node_prop("/tagged.txt", "test:tag").unwrap();
assert_eq!(val.as_deref(), Some(b"my-value".as_ref()));
let missing = rev_root.node_prop("/tagged.txt", "test:missing").unwrap();
assert_eq!(missing, None);
}
#[test]
fn test_root_revision() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root0 = fs.revision_root(Revnum(0)).unwrap();
assert_eq!(root0.revision(), Revnum(0));
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/a.txt").unwrap();
let rev1 = txn.commit().unwrap();
let root1 = fs.revision_root(rev1).unwrap();
assert_eq!(root1.revision(), Revnum(1));
}
#[test]
fn test_root_type_detection() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let rev_root = fs.revision_root(Revnum(0)).unwrap();
assert!(rev_root.is_revision_root());
assert!(!rev_root.is_txn_root());
}
#[test]
fn test_node_created_path() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/hello.txt").unwrap();
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let created_path = root.node_created_path("/hello.txt").unwrap();
assert_eq!(created_path, "/hello.txt");
}
#[test]
fn test_fs_revision_prop() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let date = fs.revision_prop(Revnum(0), "svn:date", false).unwrap();
assert!(date.is_some(), "svn:date should be set on rev 0");
let date_bytes = date.unwrap();
let date_str = std::str::from_utf8(&date_bytes).unwrap();
assert!(
date_str.contains('T'),
"svn:date should look like a timestamp: {date_str}"
);
let missing = fs
.revision_prop(Revnum(0), "custom:missing", false)
.unwrap();
assert_eq!(missing, None);
}
#[test]
fn test_fs_info_format() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let (format, version) = fs.info_format().unwrap();
assert!(format > 0, "fs format should be positive, got {format}");
let (major, minor, _patch) = version;
assert_eq!(major, 1, "SVN major version should be 1");
assert!(minor >= 9, "SVN minor version should be >= 9");
}
#[test]
fn test_fs_change_rev_prop() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.change_rev_prop(Revnum(0), "custom:test", Some(b"hello"), None)
.unwrap();
let val = fs.revision_prop(Revnum(0), "custom:test", false).unwrap();
assert_eq!(val.as_deref(), Some(b"hello" as &[u8]));
fs.change_rev_prop(
Revnum(0),
"custom:test",
Some(b"world"),
Some(Some(b"hello")),
)
.unwrap();
let val2 = fs.revision_prop(Revnum(0), "custom:test", false).unwrap();
assert_eq!(val2.as_deref(), Some(b"world" as &[u8]));
fs.change_rev_prop(Revnum(0), "custom:test", None, None)
.unwrap();
let val3 = fs.revision_prop(Revnum(0), "custom:test", false).unwrap();
assert_eq!(val3, None);
}
#[test]
fn test_fs_refresh_revision_props() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let mut fs = Fs::create(&fs_path).unwrap();
fs.refresh_revision_props().unwrap();
}
#[test]
fn test_fs_deltify_revision() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/hello.txt").unwrap();
let rev1 = txn.commit().unwrap();
fs.deltify_revision(rev1).unwrap();
}
#[test]
fn test_txn_root_name_and_base_revision() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let root = txn.root().unwrap();
let txn_name = root.txn_root_name();
assert!(!txn_name.is_empty(), "transaction name should not be empty");
let base_rev = root.txn_root_base_revision();
assert_eq!(base_rev, Revnum(0), "base revision should be 0");
}
#[test]
fn test_root_verify() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let root = fs.revision_root(Revnum(0)).unwrap();
root.verify().unwrap();
}
#[test]
fn test_revision_link() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/original.txt").unwrap();
let rev1 = txn.commit().unwrap();
let from_root = fs.revision_root(rev1).unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
{
let mut to_root = txn2.root().unwrap();
to_root.revision_link(&from_root, "/original.txt").unwrap();
}
let rev2 = txn2.commit().unwrap();
let root2 = fs.revision_root(rev2).unwrap();
let kind = root2.check_path("/original.txt").unwrap();
assert_eq!(kind, crate::NodeKind::File);
}
#[test]
fn test_root_get_mergeinfo_empty_when_not_set() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.root().unwrap().make_dir("/trunk").unwrap();
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let mut received: Vec<String> = Vec::new();
root.get_mergeinfo(
&["/trunk"],
crate::mergeinfo::MergeinfoInheritance::Explicit,
false,
false,
|path, _mi| {
received.push(path.to_owned());
Ok(())
},
)
.unwrap();
assert_eq!(received, Vec::<String>::new());
}
#[test]
fn test_root_get_mergeinfo_returns_value_when_set() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.root().unwrap().make_dir("/trunk").unwrap();
let rev1 = txn.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
txn2.root()
.unwrap()
.change_node_prop("/trunk", "svn:mergeinfo", b"/branches/dev:1")
.unwrap();
let rev2 = txn2.commit().unwrap();
let root = fs.revision_root(rev2).unwrap();
let mut received: Vec<(String, String)> = Vec::new();
root.get_mergeinfo(
&["/trunk"],
crate::mergeinfo::MergeinfoInheritance::Explicit,
false,
false,
|path, mi| {
received.push((path.to_owned(), mi.to_string().unwrap()));
Ok(())
},
)
.unwrap();
assert_eq!(received.len(), 1);
assert_eq!(received[0].0, "/trunk");
assert!(
received[0].1.contains("/branches/dev"),
"mergeinfo should contain source: {}",
received[0].1
);
}
#[test]
fn test_fs_version() {
let v = super::version();
assert!(v.major() >= 1, "major version should be >= 1");
}
#[test]
fn test_print_modules() {
let modules = super::print_modules().unwrap();
assert!(
modules.contains("fs_fs"),
"Expected fs_fs module in: {}",
modules
);
}
#[test]
fn test_info_config_files() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let files = super::info_config_files(&fs_path).expect("info_config_files should not fail");
for p in &files {
assert!(p.exists(), "Config file should exist: {:?}", p);
}
}
#[test]
fn test_fs_upgrade_already_current() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let result = super::upgrade(&fs_path, None, None);
match result {
Ok(()) => {}
Err(e) => {
assert!(
e.to_string().contains("upgrade") || e.to_string().contains("Unsupported"),
"unexpected error: {}",
e
);
}
}
}
#[test]
fn test_fs_upgrade_with_notify() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
Fs::create(&fs_path).unwrap();
let actions = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let actions_clone = actions.clone();
let result = super::upgrade(
&fs_path,
Some(Box::new(move |number, action| {
actions_clone.lock().unwrap().push((number, action));
})),
None,
);
match result {
Ok(()) => {}
Err(e) => {
assert!(
e.to_string().contains("upgrade") || e.to_string().contains("Unsupported"),
"unexpected error: {}",
e
);
}
}
}
#[test]
fn test_repos_change_txn_prop() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.repos_change_prop("svn:log", Some(b"hello from repos"))
.unwrap();
assert_eq!(
txn.prop("svn:log").unwrap().as_deref(),
Some(b"hello from repos" as &[u8])
);
txn.repos_change_prop("svn:log", None).unwrap();
let val = txn.prop("svn:log").unwrap();
assert!(val.is_none());
}
#[test]
fn test_repos_change_txn_props_bulk() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.repos_change_props(&[
("svn:log", Some(b"bulk" as &[u8])),
("svn:author", Some(b"bob" as &[u8])),
])
.unwrap();
assert_eq!(
txn.prop("svn:log").unwrap().as_deref(),
Some(b"bulk" as &[u8])
);
assert_eq!(
txn.prop("svn:author").unwrap().as_deref(),
Some(b"bob" as &[u8])
);
}
#[test]
fn test_repos_change_node_prop_and_get_inherited_props() {
let dir = tempdir().unwrap();
let fs_path = dir.path().join("test-fs");
let fs = Fs::create(&fs_path).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "init").unwrap();
{
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_dir("/trunk/src").unwrap();
root.repos_change_node_prop("/trunk", "test:prop", Some(b"trunk-value"))
.unwrap();
}
txn.commit().unwrap();
let rev_root = fs.revision_root(Revnum(1)).unwrap();
let val = rev_root.node_prop("/trunk", "test:prop").unwrap();
assert_eq!(val.as_deref(), Some(b"trunk-value" as &[u8]));
let mut txn2 = fs.begin_txn(Revnum(1), 0).unwrap();
let root2 = txn2.root().unwrap();
let inherited = root2.repos_get_inherited_props("/trunk/src", None).unwrap();
let trunk_entry = inherited
.iter()
.find(|(path, _)| path == "trunk" || path == "/trunk");
assert!(
trunk_entry.is_some(),
"expected /trunk in inherited props, got: {:?}",
inherited.iter().map(|(p, _)| p).collect::<Vec<_>>()
);
let (_, trunk_props) = trunk_entry.unwrap();
assert_eq!(
trunk_props.get("test:prop").map(|v| v.as_slice()),
Some(b"trunk-value" as &[u8])
);
}
#[test]
fn test_root_pool_lifecycle_stress() {
let dir = tempdir().unwrap();
let fs = Fs::create(&dir.path().join("test-fs")).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "init").unwrap();
let mut root = txn.root().unwrap();
root.make_file("/file.txt").unwrap();
root.set_file_contents("/file.txt", b"hello").unwrap();
txn.commit().unwrap();
for _ in 0..50 {
let root = fs.revision_root(Revnum(1)).unwrap();
assert!(root.is_file("/file.txt").unwrap());
}
let dir2 = tempdir().unwrap();
let fs2 = Fs::create(&dir2.path().join("test-fs-2")).unwrap();
let mut txn2 = fs2.begin_txn(Revnum(0), 0).unwrap();
txn2.change_prop("svn:log", "test").unwrap();
let mut root2 = txn2.root().unwrap();
root2.make_file("/file2.txt").unwrap();
root2.set_file_contents("/file2.txt", b"world").unwrap();
txn2.commit().unwrap();
let rev_root = fs2.revision_root(Revnum(1)).unwrap();
let mut contents = Vec::new();
rev_root
.file_contents("/file2.txt")
.unwrap()
.read_to_end(&mut contents)
.unwrap();
assert_eq!(contents, b"world");
}
#[test]
fn test_root_closest_copy_then_txn_write() {
let dir = tempdir().unwrap();
let fs = Fs::create(&dir.path().join("test-fs")).unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
txn.change_prop("svn:log", "rev1").unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_file("/trunk/file.txt").unwrap();
root.set_file_contents("/trunk/file.txt", b"original")
.unwrap();
txn.commit().unwrap();
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
txn.change_prop("svn:log", "branch").unwrap();
let mut root = txn.root().unwrap();
let rev1_root = fs.revision_root(Revnum(1)).unwrap();
root.copy(&rev1_root, "/trunk", "/branch").unwrap();
drop(rev1_root); txn.commit().unwrap();
let rev2_root = fs.revision_root(Revnum(2)).unwrap();
let result = rev2_root.closest_copy("/branch").unwrap();
assert!(result.is_some());
let (_copy_root, copy_path) = result.unwrap();
assert_eq!(copy_path, "/branch");
drop(_copy_root); drop(rev2_root);
let mut txn = fs.begin_txn(Revnum(2), 0).unwrap();
txn.change_prop("svn:log", "edit").unwrap();
let mut root = txn.root().unwrap();
root.set_file_contents("/trunk/file.txt", b"updated")
.unwrap();
txn.commit().unwrap();
let rev3_root = fs.revision_root(Revnum(3)).unwrap();
let mut contents = Vec::new();
rev3_root
.file_contents("/trunk/file.txt")
.unwrap()
.read_to_end(&mut contents)
.unwrap();
assert_eq!(contents, b"updated");
}
}