use crate::{svn_result, with_tmp_pool, Error, Revnum};
use std::ffi::CString;
use std::marker::PhantomData;
use subversion_sys::{
svn_repos_create, svn_repos_dump_fs4, svn_repos_find_root_path, svn_repos_load_fs6,
svn_repos_recover4, svn_repos_t, svn_repos_verify_fs3,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum LoadUUID {
#[default]
Default,
Ignore,
Force,
}
impl From<subversion_sys::svn_repos_load_uuid> for LoadUUID {
fn from(load_uuid: subversion_sys::svn_repos_load_uuid) -> Self {
match load_uuid {
subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_default => Self::Default,
subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_ignore => Self::Ignore,
subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_force => Self::Force,
_ => unreachable!(),
}
}
}
impl From<LoadUUID> for subversion_sys::svn_repos_load_uuid {
fn from(load_uuid: LoadUUID) -> Self {
match load_uuid {
LoadUUID::Default => subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_default,
LoadUUID::Ignore => subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_ignore,
LoadUUID::Force => subversion_sys::svn_repos_load_uuid_svn_repos_load_uuid_force,
}
}
}
#[derive(Default)]
pub struct DumpOptions<'a> {
pub start_rev: Option<Revnum>,
pub end_rev: Option<Revnum>,
pub incremental: bool,
pub use_deltas: bool,
pub include_revprops: bool,
pub include_changes: bool,
pub notify_func: Option<&'a dyn Fn(&Notify)>,
pub filter_func:
Option<Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>> + 'a>>,
pub cancel_func: Option<&'a dyn Fn() -> Result<(), Error<'static>>>,
}
impl<'a> DumpOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_start_rev(mut self, rev: Revnum) -> Self {
self.start_rev = Some(rev);
self
}
pub fn with_end_rev(mut self, rev: Revnum) -> Self {
self.end_rev = Some(rev);
self
}
pub fn with_incremental(mut self, incremental: bool) -> Self {
self.incremental = incremental;
self
}
pub fn with_use_deltas(mut self, use_deltas: bool) -> Self {
self.use_deltas = use_deltas;
self
}
pub fn with_include_revprops(mut self, include: bool) -> Self {
self.include_revprops = include;
self
}
pub fn with_include_changes(mut self, include: bool) -> Self {
self.include_changes = include;
self
}
}
#[derive(Default)]
pub struct LoadOptions<'a> {
pub start_rev: Option<Revnum>,
pub end_rev: Option<Revnum>,
pub uuid_action: LoadUUID,
pub parent_dir: Option<&'a str>,
pub use_pre_commit_hook: bool,
pub use_post_commit_hook: bool,
pub validate_props: bool,
pub ignore_dates: bool,
pub normalize_props: bool,
pub notify_func: Option<&'a dyn Fn(&Notify)>,
pub cancel_func: Option<&'a dyn Fn() -> Result<(), Error<'static>>>,
}
impl<'a> LoadOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_start_rev(mut self, rev: Revnum) -> Self {
self.start_rev = Some(rev);
self
}
pub fn with_end_rev(mut self, rev: Revnum) -> Self {
self.end_rev = Some(rev);
self
}
pub fn with_uuid_action(mut self, action: LoadUUID) -> Self {
self.uuid_action = action;
self
}
pub fn with_parent_dir(mut self, dir: &'a str) -> Self {
self.parent_dir = Some(dir);
self
}
pub fn with_use_pre_commit_hook(mut self, use_hook: bool) -> Self {
self.use_pre_commit_hook = use_hook;
self
}
pub fn with_use_post_commit_hook(mut self, use_hook: bool) -> Self {
self.use_post_commit_hook = use_hook;
self
}
pub fn with_validate_props(mut self, validate: bool) -> Self {
self.validate_props = validate;
self
}
pub fn with_ignore_dates(mut self, ignore: bool) -> Self {
self.ignore_dates = ignore;
self
}
pub fn with_normalize_props(mut self, normalize: bool) -> Self {
self.normalize_props = normalize;
self
}
}
#[derive(Default)]
pub struct VerifyOptions<'a> {
pub start_rev: Revnum,
pub end_rev: Revnum,
pub check_normalization: bool,
pub metadata_only: bool,
pub notify_func: Option<&'a dyn Fn(&Notify)>,
pub verify_callback: Option<&'a dyn Fn(Revnum, &Error) -> Result<(), Error<'static>>>,
pub cancel_func: Option<&'a dyn Fn() -> Result<(), Error<'static>>>,
}
impl<'a> VerifyOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_start_rev(mut self, rev: Revnum) -> Self {
self.start_rev = rev;
self
}
pub fn with_end_rev(mut self, rev: Revnum) -> Self {
self.end_rev = rev;
self
}
pub fn with_check_normalization(mut self, check: bool) -> Self {
self.check_normalization = check;
self
}
pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
self.metadata_only = metadata_only;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthzAccess {
None,
Read,
Write,
ReadWrite,
}
impl AuthzAccess {
pub fn from_raw(access: subversion_sys::svn_repos_authz_access_t) -> Self {
let read = subversion_sys::svn_repos_authz_access_t_svn_authz_read;
let write = subversion_sys::svn_repos_authz_access_t_svn_authz_write;
if access & (read | write) == (read | write) {
AuthzAccess::ReadWrite
} else if access & write != 0 {
AuthzAccess::Write
} else if access & read != 0 {
AuthzAccess::Read
} else {
AuthzAccess::None
}
}
}
impl From<AuthzAccess> for subversion_sys::svn_repos_authz_access_t {
fn from(access: AuthzAccess) -> Self {
match access {
AuthzAccess::None => subversion_sys::svn_repos_authz_access_t_svn_authz_none,
AuthzAccess::Read => subversion_sys::svn_repos_authz_access_t_svn_authz_read,
AuthzAccess::Write => subversion_sys::svn_repos_authz_access_t_svn_authz_write,
AuthzAccess::ReadWrite => {
subversion_sys::svn_repos_authz_access_t_svn_authz_read
| subversion_sys::svn_repos_authz_access_t_svn_authz_write
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RevisionAccessLevel {
None,
Partial,
Full,
}
impl From<subversion_sys::svn_repos_revision_access_level_t> for RevisionAccessLevel {
fn from(level: subversion_sys::svn_repos_revision_access_level_t) -> Self {
if level == subversion_sys::svn_repos_revision_access_level_t_svn_repos_revision_access_none
{
RevisionAccessLevel::None
} else if level
== subversion_sys::svn_repos_revision_access_level_t_svn_repos_revision_access_partial
{
RevisionAccessLevel::Partial
} else {
RevisionAccessLevel::Full
}
}
}
pub struct Authz {
ptr: *mut subversion_sys::svn_authz_t,
_pool: apr::Pool<'static>,
}
impl Authz {
pub fn read(
path: &std::path::Path,
groups_path: Option<&std::path::Path>,
must_exist: bool,
) -> Result<Self, Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let groups_path_cstr =
groups_path.map(|p| std::ffi::CString::new(p.to_str().unwrap()).unwrap());
let mut authz_ptr: *mut subversion_sys::svn_authz_t = std::ptr::null_mut();
let ret = unsafe {
subversion_sys::svn_repos_authz_read4(
&mut authz_ptr,
path_cstr.as_ptr(),
groups_path_cstr
.as_ref()
.map(|p| p.as_ptr())
.unwrap_or(std::ptr::null()),
must_exist.into(),
std::ptr::null_mut(),
None,
std::ptr::null_mut(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
Ok(Authz {
ptr: authz_ptr,
_pool: pool,
})
}
pub fn parse(contents: &str, groups_contents: Option<&str>) -> Result<Self, Error<'static>> {
let pool = apr::Pool::new();
let mut authz_ptr: *mut subversion_sys::svn_authz_t = std::ptr::null_mut();
let mut contents_stream = crate::io::Stream::from(contents.as_bytes());
let mut groups_stream = groups_contents.map(|g| crate::io::Stream::from(g.as_bytes()));
let ret = unsafe {
subversion_sys::svn_repos_authz_parse2(
&mut authz_ptr,
contents_stream.as_mut_ptr(),
groups_stream
.as_mut()
.map_or(std::ptr::null_mut(), |s| s.as_mut_ptr()),
None, std::ptr::null_mut(), pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
Ok(Authz {
ptr: authz_ptr,
_pool: pool,
})
}
pub fn check_access(
&self,
repos_name: Option<&str>,
path: &str,
user: Option<&str>,
required_access: AuthzAccess,
) -> Result<bool, Error<'static>> {
let repos_name_cstr = repos_name.map(|r| std::ffi::CString::new(r).unwrap());
let path_cstr = std::ffi::CString::new(path).unwrap();
let user_cstr = user.map(|u| std::ffi::CString::new(u).unwrap());
let mut access_granted: subversion_sys::svn_boolean_t = 0;
with_tmp_pool(|pool| {
let ret = unsafe {
subversion_sys::svn_repos_authz_check_access(
self.ptr,
repos_name_cstr
.as_ref()
.map(|r| r.as_ptr())
.unwrap_or(std::ptr::null()),
path_cstr.as_ptr(),
user_cstr
.as_ref()
.map(|u| u.as_ptr())
.unwrap_or(std::ptr::null()),
required_access.into(),
&mut access_granted,
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
Ok(access_granted != 0)
})
}
}
pub fn find_root_path(path: &std::path::Path) -> Option<std::path::PathBuf> {
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
with_tmp_pool(|pool| {
let ret = unsafe { svn_repos_find_root_path(path.as_ptr(), pool.as_mut_ptr()) };
if ret.is_null() {
None
} else {
let root_path = unsafe { std::ffi::CStr::from_ptr(ret) };
Some(std::path::PathBuf::from(root_path.to_str().unwrap()))
}
})
}
pub struct Repos {
ptr: *mut svn_repos_t,
_pool: apr::Pool<'static>,
_phantom: PhantomData<*mut ()>, }
pub fn dir_delta2(
src_root: &crate::fs::Root,
src_parent_dir: &str,
src_entry: &str,
tgt_root: &crate::fs::Root,
tgt_path: &str,
editor: &crate::delta::WrapEditor,
text_deltas: bool,
depth: crate::Depth,
entry_props: bool,
ignore_ancestry: bool,
) -> Result<(), crate::Error<'static>> {
let src_parent_dir_cstr = std::ffi::CString::new(src_parent_dir)?;
let src_entry_cstr = std::ffi::CString::new(src_entry)?;
let tgt_path_cstr = std::ffi::CString::new(tgt_path)?;
with_tmp_pool(|pool| {
let (editor_ptr, edit_baton) = editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_repos_dir_delta2(
src_root.as_ptr() as *mut _,
src_parent_dir_cstr.as_ptr(),
src_entry_cstr.as_ptr(),
tgt_root.as_ptr() as *mut _,
tgt_path_cstr.as_ptr(),
editor_ptr,
edit_baton,
None, std::ptr::null_mut(), if text_deltas { 1 } else { 0 },
depth.into(),
if entry_props { 1 } else { 0 },
if ignore_ancestry { 1 } else { 0 },
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
#[cfg(feature = "delta")]
pub fn replay(
root: &crate::fs::Root,
base_dir: &str,
low_water_mark: Revnum,
send_deltas: bool,
editor: &crate::delta::WrapEditor,
) -> Result<(), Error<'static>> {
let base_dir_cstr = std::ffi::CString::new(base_dir)?;
with_tmp_pool(|pool| {
let (editor_ptr, edit_baton) = editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_repos_replay2(
root.as_ptr() as *mut _,
base_dir_cstr.as_ptr(),
low_water_mark.0,
if send_deltas { 1 } else { 0 },
editor_ptr,
edit_baton,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
impl Drop for Repos {
fn drop(&mut self) {
}
}
unsafe fn drop_authz_commit_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton
as *mut Box<
dyn FnMut(AuthzAccess, &crate::fs::Root, &str) -> Result<bool, Error<'static>>,
>,
));
}
unsafe fn drop_authz_read_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
));
}
impl Repos {
pub fn create(path: &std::path::Path) -> Result<Repos, Error<'static>> {
Self::create_with_config(path, None, None)
}
pub fn create_with_config(
path: &std::path::Path,
config: Option<&std::collections::HashMap<String, String>>,
fs_config: Option<&std::collections::HashMap<String, String>>,
) -> Result<Repos, Error<'static>> {
crate::init::initialize()?;
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let config_hash = if let Some(cfg) = config {
let byte_pairs: Vec<_> = cfg
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_bytes()))
.collect();
let mut hash = apr::hash::Hash::new(&pool);
for (k, v) in byte_pairs.iter() {
unsafe {
hash.insert(k, v.as_ptr() as *mut std::ffi::c_void);
}
}
unsafe { hash.as_mut_ptr() }
} else {
std::ptr::null_mut()
};
let fs_config_hash = if let Some(cfg) = fs_config {
let byte_pairs: Vec<_> = cfg
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_bytes()))
.collect();
let mut hash = apr::hash::Hash::new(&pool);
for (k, v) in byte_pairs.iter() {
unsafe {
hash.insert(k, v.as_ptr() as *mut std::ffi::c_void);
}
}
unsafe { hash.as_mut_ptr() }
} else {
std::ptr::null_mut()
};
unsafe {
let mut repos: *mut svn_repos_t = std::ptr::null_mut();
let ret = svn_repos_create(
&mut repos,
path.as_ptr(),
std::ptr::null(),
std::ptr::null(),
config_hash,
fs_config_hash,
pool.as_mut_ptr(),
);
svn_result(ret)?;
Ok(Repos {
ptr: repos,
_pool: pool,
_phantom: PhantomData,
})
}
}
pub fn open(path: &std::path::Path) -> Result<Repos, Error<'static>> {
crate::init::initialize()?;
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
unsafe {
let mut repos: *mut svn_repos_t = std::ptr::null_mut();
let ret = subversion_sys::svn_repos_open(&mut repos, path.as_ptr(), pool.as_mut_ptr());
svn_result(ret)?;
Ok(Repos {
ptr: repos,
_pool: pool,
_phantom: PhantomData,
})
}
}
pub fn capabilities(&mut self) -> Result<std::collections::HashSet<String>, Error<'_>> {
let pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let mut capabilities: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
let ret = unsafe {
subversion_sys::svn_repos_capabilities(
&mut capabilities,
self.ptr,
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
let capabilities_hash = unsafe { apr::hash::Hash::from_ptr(capabilities) };
Ok(capabilities_hash
.iter()
.map(|(k, _)| String::from_utf8_lossy(k).to_string())
.collect::<std::collections::HashSet<String>>())
}
pub fn has_capability(&mut self, capability: &str) -> Result<bool, Error<'static>> {
let capability = std::ffi::CString::new(capability).unwrap();
let pool = apr::Pool::new();
unsafe {
let mut has: subversion_sys::svn_boolean_t = 0;
let ret = subversion_sys::svn_repos_has_capability(
self.ptr,
&mut has,
capability.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(ret)?;
Ok(has != 0)
}
}
pub fn remember_client_capabilities(
&mut self,
capabilities: &[&str],
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let capabilities = capabilities
.iter()
.map(|c| std::ffi::CString::new(*c).unwrap())
.collect::<Vec<_>>();
let mut capabilities_array =
apr::tables::TypedArray::<*const i8>::new(&pool, capabilities.len() as i32);
for cap in capabilities.iter() {
capabilities_array.push(cap.as_ptr());
}
let ret = unsafe {
subversion_sys::svn_repos_remember_client_capabilities(
self.ptr,
capabilities_array.as_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn fs(&self) -> Option<crate::fs::Fs<'static>> {
let fs_ptr = unsafe { subversion_sys::svn_repos_fs(self.ptr) };
if fs_ptr.is_null() {
None
} else {
let pool = apr::Pool::new();
Some(unsafe { crate::fs::Fs::from_ptr_and_pool(fs_ptr, pool) })
}
}
pub fn fs_type(&self) -> String {
with_tmp_pool(|pool| {
let ret = unsafe { subversion_sys::svn_repos_fs_type(self.ptr, pool.as_mut_ptr()) };
let fs_type = unsafe { std::ffi::CStr::from_ptr(ret) };
fs_type.to_str().unwrap().to_string()
})
}
pub fn path(&mut self) -> std::path::PathBuf {
with_tmp_pool(|pool| {
let ret = unsafe { subversion_sys::svn_repos_path(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(path.to_str().unwrap())
})
}
pub fn db_env(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_db_env(self.ptr, pool.as_mut_ptr()) };
let db_env = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(db_env.to_str().unwrap())
}
pub fn conf_dir(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_conf_dir(self.ptr, pool.as_mut_ptr()) };
let conf_dir = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(conf_dir.to_str().unwrap())
}
pub fn svnserve_conf(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_svnserve_conf(self.ptr, pool.as_mut_ptr()) };
let svnserve_conf = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(svnserve_conf.to_str().unwrap())
}
pub fn lock_dir(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_lock_dir(self.ptr, pool.as_mut_ptr()) };
let lock_dir = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(lock_dir.to_str().unwrap())
}
pub fn db_lockfile(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_db_lockfile(self.ptr, pool.as_mut_ptr()) };
let db_lockfile = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(db_lockfile.to_str().unwrap())
}
pub fn db_logs_lockfile(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret =
unsafe { subversion_sys::svn_repos_db_logs_lockfile(self.ptr, pool.as_mut_ptr()) };
let logs_lockfile = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(logs_lockfile.to_str().unwrap())
}
pub fn hook_dir(&mut self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_hook_dir(self.ptr, pool.as_mut_ptr()) };
let hook_dir = unsafe { std::ffi::CStr::from_ptr(ret) };
std::path::PathBuf::from(hook_dir.to_str().unwrap())
}
pub fn load_fs(
&self,
dumpstream: &mut crate::io::Stream,
start_rev: Option<Revnum>,
end_rev: Option<Revnum>,
uuid_action: LoadUUID,
parent_dir: &std::path::Path,
use_pre_commit_hook: bool,
use_post_commit_hook: bool,
cancel_check: Option<&impl Fn() -> bool>,
validate_props: bool,
ignore_dates: bool,
normalize_props: bool,
notify_func: Option<&impl Fn(&Notify)>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let parent_dir_cstring = if parent_dir.as_os_str().is_empty() {
None
} else {
Some(std::ffi::CString::new(parent_dir.to_str().unwrap())?)
};
let parent_dir_cstr = match &parent_dir_cstring {
Some(cstr) => cstr.as_ptr(),
None => std::ptr::null(),
};
let ret = unsafe {
subversion_sys::svn_repos_load_fs6(
self.ptr,
dumpstream.as_mut_ptr(),
start_rev.map(|r| r.0).unwrap_or(-1), end_rev.map(|r| r.0).unwrap_or(-1), uuid_action.into(),
parent_dir_cstr,
use_pre_commit_hook.into(),
use_post_commit_hook.into(),
validate_props.into(),
ignore_dates.into(),
normalize_props.into(),
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
if cancel_check.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_check
.map(|cancel_check| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
Box::new(move || {
if cancel_check() {
Err(Error::from_message("Operation cancelled"))
} else {
Ok(())
}
});
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn load_fs_revprops(
&self,
dumpstream: &mut crate::io::Stream,
start_rev: Option<Revnum>,
end_rev: Option<Revnum>,
validate_props: bool,
ignore_dates: bool,
normalize_props: bool,
notify_func: Option<&impl Fn(&Notify)>,
cancel_check: Option<&impl Fn() -> bool>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_load_fs_revprops(
self.ptr,
dumpstream.as_mut_ptr(),
start_rev.map(|r| r.0).unwrap_or(-1),
end_rev.map(|r| r.0).unwrap_or(-1),
validate_props.into(),
ignore_dates.into(),
normalize_props.into(),
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
if cancel_check.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_check
.map(|cancel_check| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
Box::new(move || {
if cancel_check() {
Err(Error::from_message("Operation cancelled"))
} else {
Ok(())
}
});
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn fs_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<'static>> {
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 pool = apr::Pool::new();
let mut lock_ptr: *mut subversion_sys::svn_lock_t = std::ptr::null_mut();
let ret = unsafe {
subversion_sys::svn_repos_fs_lock(
&mut lock_ptr,
self.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(),
)
};
Error::from_raw(ret)?;
let pool_handle = apr::PoolHandle::owned(pool);
Ok(crate::Lock::from_raw(lock_ptr, pool_handle))
}
pub fn fs_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 = std::ffi::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_repos_fs_lock_many(
self.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,
);
Error::from_raw(err)?;
}
Ok(())
}
pub fn fs_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 pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_fs_unlock(
self.ptr,
path_cstr.as_ptr(),
token_cstr.as_ptr(),
break_lock as i32,
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn fs_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 = std::ffi::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_repos_fs_unlock_many(
self.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,
);
Error::from_raw(err)?;
}
Ok(())
}
pub fn fs_get_locks(
&self,
path: &str,
depth: crate::Depth,
authz_read_func: Option<
Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
>,
) -> Result<Vec<crate::Lock<'static>>, Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let pool = apr::Pool::new();
let has_authz = authz_read_func.is_some();
let authz_baton = authz_read_func
.map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
extern "C" fn wrap_authz_read_func(
allowed: *mut i32,
root: *mut subversion_sys::svn_fs_root_t,
path: *const i8,
baton: *mut std::ffi::c_void,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || allowed.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&mut *(baton
as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>)
};
let path_str = unsafe {
if path.is_null() {
""
} else {
std::ffi::CStr::from_ptr(path).to_str().unwrap_or("")
}
};
let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };
match callback(&fs_root, path_str) {
Ok(is_allowed) => {
unsafe {
*allowed = if is_allowed { 1 } else { 0 };
}
std::ptr::null_mut()
}
Err(mut e) => unsafe { e.detach() },
}
}
let mut locks_hash: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
let ret = unsafe {
subversion_sys::svn_repos_fs_get_locks2(
&mut locks_hash,
self.ptr,
path_cstr.as_ptr(),
depth.into(),
if has_authz {
Some(wrap_authz_read_func)
} else {
None
},
authz_baton,
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
let mut locks = Vec::new();
if !locks_hash.is_null() {
unsafe {
let mut hi = apr_sys::apr_hash_first(pool.as_mut_ptr(), locks_hash);
while !hi.is_null() {
let mut val_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
apr_sys::apr_hash_this(
hi,
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut val_ptr,
);
if !val_ptr.is_null() {
let lock_ptr = val_ptr as *const subversion_sys::svn_lock_t;
let lock_pool = apr::Pool::new();
let dup_ptr =
subversion_sys::svn_lock_dup(lock_ptr, lock_pool.as_mut_ptr());
if !dup_ptr.is_null() {
let pool_handle = apr::PoolHandle::owned(lock_pool);
locks.push(crate::Lock::from_raw(dup_ptr, pool_handle));
}
}
hi = apr_sys::apr_hash_next(hi);
}
}
}
Ok(locks)
}
pub fn fs_get_inherited_props(
&self,
root: &mut crate::fs::Root,
path: &str,
propname: Option<&str>,
) -> Result<Vec<(String, std::collections::HashMap<String, Vec<u8>>)>, Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let propname_cstr =
propname.map(|p| std::ffi::CString::new(p).expect("propname must be valid UTF-8"));
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();
let err = unsafe {
subversion_sys::svn_repos_fs_get_inherited_props(
&mut inherited_props,
root.as_mut_ptr(),
path_cstr.as_ptr(),
propname_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
None, std::ptr::null_mut(), result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)?;
let mut result = Vec::new();
if !inherited_props.is_null() {
let array = unsafe {
apr::tables::TypedArray::<*mut subversion_sys::svn_prop_inherited_item_t>::from_ptr(
inherited_props,
)
};
for item_ptr in array.iter() {
if item_ptr.is_null() {
continue;
}
unsafe {
let item = *item_ptr;
let path_or_url = if item.path_or_url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(item.path_or_url)
.to_string_lossy()
.into_owned()
};
let props = if item.prop_hash.is_null() {
std::collections::HashMap::new()
} else {
let prop_hash = crate::props::PropHash::from_ptr(item.prop_hash);
prop_hash.to_hashmap()
};
result.push((path_or_url, props));
}
}
}
Ok(result)
}
pub fn verify_fs(
&self,
start_rev: Revnum,
end_rev: Revnum,
check_normalization: bool,
metadata_only: bool,
notify_func: Option<&impl Fn(&Notify)>,
callback_func: &impl Fn(Revnum, &Error) -> Result<(), Error<'static>>,
cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
extern "C" fn verify_callback(
baton: *mut std::ffi::c_void,
revision: subversion_sys::svn_revnum_t,
verify_err: *mut subversion_sys::svn_error_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = unsafe {
&mut *(baton as *mut Box<dyn FnMut(Revnum, &Error) -> Result<(), Error<'static>>>)
};
let verify_err = Error::from_raw(verify_err).unwrap_err();
match baton(Revnum::from_raw(revision).unwrap(), &verify_err) {
Ok(()) => std::ptr::null_mut(),
Err(e) => unsafe { e.into_raw() },
}
}
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_verify_fs3(
self.ptr,
start_rev.0,
end_rev.0,
check_normalization.into(),
metadata_only.into(),
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
Some(verify_callback),
Box::into_raw(Box::new(callback_func)) as *mut std::ffi::c_void,
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn pack_fs(
&self,
notify_func: Option<&impl Fn(&Notify)>,
cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_fs_pack2(
self.ptr,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> =
Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn uuid(&self) -> Result<String, Error<'static>> {
let pool = apr::Pool::new();
let mut uuid_ptr = std::ptr::null();
let ret = unsafe {
subversion_sys::svn_fs_get_uuid(
subversion_sys::svn_repos_fs(self.ptr),
&mut uuid_ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
if uuid_ptr.is_null() {
Ok(String::new())
} else {
let uuid_cstr = unsafe { std::ffi::CStr::from_ptr(uuid_ptr) };
Ok(uuid_cstr.to_str()?.to_string())
}
}
pub fn set_uuid(&self, uuid: &str) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let uuid_cstr = std::ffi::CString::new(uuid)?;
let ret = unsafe {
subversion_sys::svn_fs_set_uuid(
subversion_sys::svn_repos_fs(self.ptr),
uuid_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn youngest_rev(&self) -> Result<Revnum, Error<'static>> {
let pool = apr::Pool::new();
let mut revnum: subversion_sys::svn_revnum_t = 0;
let ret = unsafe {
subversion_sys::svn_fs_youngest_rev(
&mut revnum,
subversion_sys::svn_repos_fs(self.ptr),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(Revnum(revnum))
}
pub fn begin_txn_for_commit(
&self,
revnum: Revnum,
author: &str,
log_msg: &str,
) -> Result<crate::fs::Transaction<'_>, Error<'static>> {
let pool = apr::Pool::new();
let mut txn_ptr = std::ptr::null_mut();
let mut revprop_table = apr::hash::Hash::new(&pool);
let author_key = b"svn:author";
let author_val = crate::svn_string_helpers::svn_string_ncreate(author.as_bytes(), &pool);
unsafe {
revprop_table.insert(author_key, author_val as *mut std::ffi::c_void);
}
let log_key = b"svn:log";
let log_val = crate::svn_string_helpers::svn_string_ncreate(log_msg.as_bytes(), &pool);
unsafe {
revprop_table.insert(log_key, log_val as *mut std::ffi::c_void);
}
let ret = unsafe {
subversion_sys::svn_repos_fs_begin_txn_for_commit2(
&mut txn_ptr,
self.ptr,
revnum.0,
revprop_table.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(unsafe { crate::fs::Transaction::from_ptr_and_pool(txn_ptr, pool) })
}
pub fn commit_txn(
&self,
txn: crate::fs::Transaction<'_>,
) -> Result<(Revnum, Option<String>), Error<'static>> {
let pool = apr::Pool::new();
let mut new_rev: subversion_sys::svn_revnum_t = -1; let mut conflict_p: *const std::os::raw::c_char = std::ptr::null();
let err = unsafe {
subversion_sys::svn_repos_fs_commit_txn(
&mut conflict_p,
self.ptr,
&mut new_rev,
txn.as_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)?;
let conflict = if conflict_p.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(conflict_p) }
.to_string_lossy()
.into_owned(),
)
};
Ok((Revnum(new_rev), conflict))
}
pub fn rev_proplist(
&self,
revnum: Revnum,
) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
let pool = apr::Pool::new();
let mut props_ptr = std::ptr::null_mut();
extern "C" fn authz_read_func(
_allowed: *mut i32,
_root: *mut subversion_sys::svn_fs_root_t,
_path: *const std::ffi::c_char,
_baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
*_allowed = 1; }
std::ptr::null_mut() }
let ret = unsafe {
subversion_sys::svn_repos_fs_revision_proplist(
&mut props_ptr,
self.ptr,
revnum.0,
Some(authz_read_func),
std::ptr::null_mut(),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
if props_ptr.is_null() {
Ok(std::collections::HashMap::new())
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props_ptr) };
Ok(prop_hash.to_hashmap())
}
}
pub fn rev_prop(&self, revnum: Revnum, name: &str) -> Result<Option<Vec<u8>>, Error<'static>> {
let name_cstr = std::ffi::CString::new(name)?;
extern "C" fn authz_always_allow(
allowed: *mut subversion_sys::svn_boolean_t,
_root: *mut subversion_sys::svn_fs_root_t,
_path: *const std::os::raw::c_char,
_baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe { *allowed = 1 };
std::ptr::null_mut()
}
with_tmp_pool(|pool| {
let mut value_p: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_repos_fs_revision_prop(
&mut value_p,
self.ptr,
revnum.0,
name_cstr.as_ptr(),
Some(authz_always_allow),
std::ptr::null_mut(),
pool.as_mut_ptr(),
)
};
svn_result(err)?;
if value_p.is_null() {
return Ok(None);
}
let data = unsafe {
let s = &*value_p;
std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec()
};
Ok(Some(data))
})
}
pub fn change_rev_prop(
&self,
revnum: Revnum,
name: &str,
value: Option<&[u8]>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let name_cstr = std::ffi::CString::new(name)?;
let value_ptr = if let Some(val) = value {
crate::svn_string_helpers::svn_string_ncreate(val, &pool)
} else {
std::ptr::null_mut()
};
extern "C" fn authz_read_func(
_allowed: *mut i32,
_root: *mut subversion_sys::svn_fs_root_t,
_path: *const std::ffi::c_char,
_baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
*_allowed = 1; }
std::ptr::null_mut()
}
let ret = unsafe {
subversion_sys::svn_repos_fs_change_rev_prop4(
self.ptr,
revnum.0,
std::ptr::null(), name_cstr.as_ptr(),
std::ptr::null_mut(), value_ptr,
1, 1, Some(authz_read_func),
std::ptr::null_mut(),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn dump(
&self,
stream: &mut crate::io::Stream,
options: &mut DumpOptions,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let notify_baton = options
.notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let has_filter = options.filter_func.is_some();
let filter_baton = options
.filter_func
.take()
.map(|f| Box::into_raw(Box::new(f)) as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
let cancel_baton = options
.cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
svn_repos_dump_fs4(
self.ptr,
stream.as_mut_ptr(),
options.start_rev.map(|r| r.into()).unwrap_or(-1),
options.end_rev.map(|r| r.into()).unwrap_or(-1),
options.incremental.into(),
options.use_deltas.into(),
options.include_revprops.into(),
options.include_changes.into(),
if options.notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
if has_filter {
Some(wrap_filter_func)
} else {
None
},
filter_baton,
if options.cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
pool.as_mut_ptr(),
)
};
if !notify_baton.is_null() {
unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
}
if !filter_baton.is_null() {
unsafe {
drop(Box::from_raw(
filter_baton
as *mut Box<
dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>,
>,
));
}
}
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
Error::from_raw(ret)?;
Ok(())
}
pub fn load(
&self,
dumpstream: &mut crate::io::Stream,
options: &LoadOptions,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let parent_dir_cstr = options
.parent_dir
.map(|p| std::ffi::CString::new(p).unwrap());
let parent_dir_ptr = parent_dir_cstr
.as_ref()
.map(|s| s.as_ptr())
.unwrap_or(std::ptr::null());
let notify_baton = options
.notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let cancel_baton = options
.cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
svn_repos_load_fs6(
self.ptr,
dumpstream.as_mut_ptr(),
options.start_rev.map(|r| r.into()).unwrap_or(-1),
options.end_rev.map(|r| r.into()).unwrap_or(-1),
options.uuid_action.into(),
parent_dir_ptr,
options.use_pre_commit_hook.into(),
options.use_post_commit_hook.into(),
options.validate_props.into(),
options.ignore_dates.into(),
options.normalize_props.into(),
if options.notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
if options.cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
pool.as_mut_ptr(),
)
};
if !notify_baton.is_null() {
unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
}
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
Error::from_raw(ret)?;
Ok(())
}
pub fn verify(&self, options: &VerifyOptions) -> Result<(), Error<'static>> {
extern "C" fn verify_error_callback(
baton: *mut std::ffi::c_void,
revision: subversion_sys::svn_revnum_t,
verify_err: *mut subversion_sys::svn_error_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = unsafe {
&mut *(baton as *mut Box<dyn FnMut(Revnum, &Error) -> Result<(), Error<'static>>>)
};
let verify_err = Error::from_raw(verify_err).unwrap_err();
match baton(Revnum::from_raw(revision).unwrap(), &verify_err) {
Ok(()) => std::ptr::null_mut(),
Err(e) => unsafe { e.into_raw() },
}
}
let pool = apr::Pool::new();
let notify_baton = options
.notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let verify_baton = options
.verify_callback
.map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
let cancel_baton = options
.cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
svn_repos_verify_fs3(
self.ptr,
options.start_rev.into(),
options.end_rev.into(),
options.check_normalization.into(),
options.metadata_only.into(),
if options.notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
if options.verify_callback.is_some() {
Some(verify_error_callback)
} else {
None
},
verify_baton,
if options.cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
pool.as_mut_ptr(),
)
};
if !notify_baton.is_null() {
unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
}
if !verify_baton.is_null() {
unsafe {
drop(Box::from_raw(
verify_baton as *mut Box<dyn Fn(Revnum, &Error) -> Result<(), Error<'static>>>,
))
};
}
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
Error::from_raw(ret)?;
Ok(())
}
pub fn recover(
&mut self,
nonblocking: bool,
notify_func: Option<&dyn Fn(&Notify)>,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let path = self.path();
let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let notify_baton = notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let cancel_baton = cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
svn_repos_recover4(
path_cstr.as_ptr(),
nonblocking.into(),
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
pool.as_mut_ptr(),
)
};
if !notify_baton.is_null() {
unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
}
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
Error::from_raw(ret)?;
Ok(())
}
}
pub fn recover(
path: &str,
nonblocking: bool,
notify_func: Option<&impl Fn(&Notify)>,
cance_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path).unwrap();
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_recover4(
path.as_ptr(),
nonblocking.into(),
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
if cance_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cance_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
extern "C" fn wrap_freeze_func(
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let freeze_func = unsafe { &*(baton as *const Box<dyn Fn() -> Result<(), Error<'static>>>) };
match freeze_func() {
Ok(()) => std::ptr::null_mut(),
Err(e) => unsafe { e.into_raw() },
}
}
pub fn freeze(
paths: &[&str],
freeze_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let paths = paths
.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect::<Vec<_>>();
let pool = apr::Pool::new();
let mut paths_array = apr::tables::TypedArray::<*const i8>::new(&pool, paths.len() as i32);
for path in paths.iter() {
paths_array.push(path.as_ptr());
}
let ret = unsafe {
subversion_sys::svn_repos_freeze(
paths_array.as_ptr(),
if freeze_func.is_some() {
Some(wrap_freeze_func)
} else {
None
},
freeze_func
.map(|freeze_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(freeze_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
#[allow(dead_code)]
pub struct Notify<'a> {
ptr: *const subversion_sys::svn_repos_notify_t,
_phantom: PhantomData<&'a ()>,
}
impl<'a> Notify<'a> {
unsafe fn from_ptr(ptr: *const subversion_sys::svn_repos_notify_t) -> Self {
Self {
ptr,
_phantom: PhantomData,
}
}
pub fn revision(&self) -> Revnum {
unsafe { Revnum((*self.ptr).revision) }
}
pub fn action(&self) -> u32 {
unsafe { (*self.ptr).action as u32 }
}
pub fn is_verify_rev_end(&self) -> bool {
self.action()
== subversion_sys::svn_repos_notify_action_t_svn_repos_notify_verify_rev_end as u32
}
}
extern "C" fn wrap_filter_func(
include: *mut i32,
root: *mut subversion_sys::svn_fs_root_t,
path: *const i8,
baton: *mut std::ffi::c_void,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || include.is_null() || root.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&mut *(baton as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>)
};
let path_str = if path.is_null() {
""
} else {
unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap_or("") }
};
let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };
match callback(&fs_root, path_str) {
Ok(should_include) => {
unsafe {
*include = if should_include { 1 } else { 0 };
}
std::ptr::null_mut()
}
Err(mut e) => unsafe { e.detach() },
}
}
extern "C" fn wrap_notify_func(
baton: *mut std::ffi::c_void,
notify: *const subversion_sys::svn_repos_notify_t,
_pool: *mut apr_sys::apr_pool_t,
) {
let baton = unsafe { &mut *(baton as *mut Box<dyn FnMut(&Notify)>) };
unsafe {
baton(&Notify::from_ptr(notify));
}
}
pub fn upgrade(
path: &std::path::Path,
nonblocking: bool,
notify_func: Option<&mut dyn FnMut(&Notify)>,
) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let notify_func = notify_func.map(|_notify_func| wrap_notify_func as _);
let notify_baton = Box::into_raw(Box::new(notify_func)).cast();
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_upgrade2(
path.as_ptr(),
nonblocking as i32,
notify_func,
notify_baton,
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
pub fn delete(path: &std::path::Path) -> Result<(), Error<'static>> {
let path = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let ret = unsafe { subversion_sys::svn_repos_delete(path.as_ptr(), pool.as_mut_ptr()) };
Error::from_raw(ret)?;
Ok(())
}
pub fn version() -> crate::Version {
unsafe { crate::Version(subversion_sys::svn_repos_version()) }
}
pub fn hotcopy(
src_path: &std::path::Path,
dst_path: &std::path::Path,
clean_logs: bool,
incremental: bool,
notify_func: Option<&impl Fn(&Notify)>,
cancel_func: Option<&impl Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let src_path = std::ffi::CString::new(src_path.to_str().unwrap()).unwrap();
let dst_path = std::ffi::CString::new(dst_path.to_str().unwrap()).unwrap();
let pool = apr::Pool::new();
let ret = unsafe {
subversion_sys::svn_repos_hotcopy3(
src_path.as_ptr(),
dst_path.as_ptr(),
clean_logs.into(),
incremental.into(),
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut()),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_delete() {
let td = tempfile::tempdir().unwrap();
super::Repos::create(td.path()).unwrap();
super::Repos::open(td.path()).unwrap();
super::delete(td.path()).unwrap();
}
#[test]
fn test_capabilities() {
let td = tempfile::tempdir().unwrap();
let mut repos = super::Repos::create(td.path()).unwrap();
assert!(repos.capabilities().unwrap().contains("mergeinfo"));
assert!(!repos.capabilities().unwrap().contains("mergeinfo2"));
assert!(repos.has_capability("mergeinfo").unwrap());
assert!(repos.has_capability("unknown").is_err());
}
#[test]
fn test_load_fs_segfault_reproduction() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let dump_data = b"SVN-fs-dump-format-version: 3\nUUID: test-uuid-1234\n\nRevision-number: 0\nProp-content-length: 56\nContent-length: 56\n\nK 8\nsvn:date\nV 27\n2023-01-01T12:00:00.000000Z\nPROPS-END\n\n";
let mut stream = crate::io::Stream::from(dump_data.as_slice());
let result = repo.load_fs(
&mut stream,
None,
None,
LoadUUID::Default,
std::path::Path::new(""),
false,
false,
None::<&fn() -> bool>,
true,
false,
false,
None::<&fn(&Notify)>,
);
println!("load_fs result: {:?}", result);
}
#[test]
fn test_version() {
assert!(super::version().major() >= 1);
}
#[test]
fn test_create_with_config() {
let td = tempfile::tempdir().unwrap();
let config = std::collections::HashMap::from([(
"repository.compression".to_string(),
"6".to_string(),
)]);
let fs_config =
std::collections::HashMap::from([("fsfs.compression".to_string(), "zlib".to_string())]);
let repos = super::Repos::create_with_config(td.path(), Some(&config), Some(&fs_config));
assert!(repos.is_ok(), "Failed to create repository with config");
let repos = super::Repos::open(td.path());
assert!(
repos.is_ok(),
"Failed to open repository created with config"
);
}
#[test]
fn test_create_with_empty_config() {
let td = tempfile::tempdir().unwrap();
let empty_config = std::collections::HashMap::new();
let repos = super::Repos::create_with_config(td.path(), Some(&empty_config), None);
assert!(
repos.is_ok(),
"Failed to create repository with empty config"
);
}
#[test]
fn test_dump_basic() {
let td = tempfile::tempdir().unwrap();
let repos = super::Repos::create(td.path()).unwrap();
let mut buffer = Vec::new();
let mut stream = crate::io::wrap_write(&mut buffer).unwrap();
let mut options = DumpOptions {
start_rev: Some(crate::Revnum(0)),
end_rev: Some(crate::Revnum(0)),
include_revprops: true,
include_changes: true,
..Default::default()
};
let result = repos.dump(&mut stream, &mut options);
result.unwrap();
let dump_str = String::from_utf8_lossy(&buffer);
assert!(
dump_str.contains("SVN-fs-dump-format-version"),
"Dump output should contain format version header"
);
}
#[test]
fn test_dump_all_revisions() {
let td = tempfile::tempdir().unwrap();
let repos = super::Repos::create(td.path()).unwrap();
let mut buffer = Vec::new();
let mut stream = crate::io::wrap_write(&mut buffer).unwrap();
let mut options = DumpOptions {
include_revprops: true,
include_changes: true,
..Default::default()
};
let result = repos.dump(&mut stream, &mut options);
result.unwrap();
}
#[test]
fn test_dump_with_filter() {
let td = tempfile::tempdir().unwrap();
let repos = super::Repos::create(td.path()).unwrap();
let fs = repos.fs().unwrap();
let mut txn = fs.begin_txn(crate::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();
stream.write(b"test content").unwrap();
stream.close().unwrap();
txn.commit().unwrap();
let mut buffer = Vec::new();
let mut stream = crate::io::wrap_write(&mut buffer).unwrap();
let filtered_paths = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let filtered_paths_clone = filtered_paths.clone();
let mut options = DumpOptions {
start_rev: Some(crate::Revnum(1)),
end_rev: Some(crate::Revnum(1)),
include_revprops: true,
include_changes: true,
filter_func: Some(Box::new(move |_root, path| {
filtered_paths_clone.lock().unwrap().push(path.to_string());
Ok(true) })),
..Default::default()
};
let result = repos.dump(&mut stream, &mut options);
result.unwrap();
let paths = filtered_paths.lock().unwrap();
assert!(!paths.is_empty(), "Filter function should have been called");
assert!(
paths.iter().any(|p| p == "test.txt" || p == "/test.txt"),
"Filter should have been called for test.txt, got paths: {:?}",
paths
);
}
#[test]
fn test_verify_basic() {
let td = tempfile::tempdir().unwrap();
let repos = super::Repos::create(td.path()).unwrap();
let options = VerifyOptions::new();
let result = repos.verify(&options);
result.unwrap();
}
#[test]
fn test_recover_basic() {
let td = tempfile::tempdir().unwrap();
let mut repos = super::Repos::create(td.path()).unwrap();
repos
.recover(
true, None, None, )
.unwrap();
}
#[test]
fn test_load_basic() {
let td = tempfile::tempdir().unwrap();
let repos = super::Repos::create(td.path()).unwrap();
let dump_content = b"SVN-fs-dump-format-version: 2\n\n\
UUID: 00000000-0000-0000-0000-000000000000\n\n\
Revision-number: 0\n\
Prop-content-length: 56\n\
Content-length: 56\n\n\
K 8\n\
svn:date\n\
V 27\n\
2024-01-01T00:00:00.000000Z\n\
PROPS-END\n\n";
let mut dump_vec = dump_content.to_vec();
let mut stream = crate::io::wrap_write(&mut dump_vec).unwrap();
let options = LoadOptions::new();
let result = repos.load(&mut stream, &options);
match result {
Ok(_) => println!("Load succeeded"),
Err(e) => println!("Load failed as expected: {}", e),
}
}
#[test]
fn test_load_fs_revprops() {
let td = tempfile::tempdir().unwrap();
let repos = super::Repos::create(td.path()).unwrap();
let mut dump_buf = Vec::new();
{
let mut stream = crate::io::wrap_write(&mut dump_buf).unwrap();
let mut options = super::DumpOptions {
start_rev: Some(crate::Revnum(0)),
end_rev: Some(crate::Revnum(0)),
include_revprops: true,
include_changes: false,
..Default::default()
};
repos.dump(&mut stream, &mut options).unwrap();
}
let mut read_stream =
crate::io::Stream::from_reader(std::io::Cursor::new(dump_buf)).unwrap();
let result = repos.load_fs_revprops(
&mut read_stream,
None,
None,
false,
false,
false,
None::<&fn(&Notify)>,
None::<&fn() -> bool>,
);
match result {
Ok(()) => {}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("format") || msg.contains("invalid") || msg.contains("parse"),
"unexpected error: {}",
msg
);
}
}
}
}
pub fn fs_pack(
path: &std::path::Path,
notify_func: Option<&dyn Fn(&Notify)>,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let notify_baton = notify_func
.map(|notify_func| {
let boxed: Box<dyn FnMut(&Notify)> = Box::new(move |n| notify_func(n));
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let cancel_baton = cancel_func
.map(|cancel_func| {
let boxed: Box<dyn Fn() -> Result<(), Error<'static>>> = Box::new(cancel_func);
Box::into_raw(Box::new(boxed)) as *mut std::ffi::c_void
})
.unwrap_or(std::ptr::null_mut());
let repos = Repos::open(path)?;
let ret = unsafe {
subversion_sys::svn_repos_fs_pack2(
repos.ptr,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
pool.as_mut_ptr(),
)
};
if !notify_baton.is_null() {
unsafe { drop(Box::from_raw(notify_baton as *mut Box<dyn Fn(&Notify)>)) };
}
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
Error::from_raw(ret)
}
impl Repos {
#[cfg(feature = "delta")]
pub fn get_commit_editor<'s>(
&'s self,
repos_url: &str,
base_path: &str,
log_msg: &str,
author: Option<&str>,
revprops: Option<&std::collections::HashMap<String, Vec<u8>>>,
commit_callback: Option<&dyn Fn(crate::Revnum, &str, &std::ffi::CStr)>,
authz_callback: Option<
Box<dyn FnMut(AuthzAccess, &crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
>,
txn: Option<&mut crate::fs::Transaction<'_>>,
) -> Result<crate::delta::WrapEditor<'s>, Error<'s>> {
let repos_url_cstr = std::ffi::CString::new(repos_url)?;
let base_path_cstr = std::ffi::CString::new(base_path)?;
let log_msg_cstr = std::ffi::CString::new(log_msg)?;
let author_cstr = author.map(std::ffi::CString::new).transpose()?;
let pool = Box::new(apr::Pool::new());
let pool_ptr = pool.as_ref().as_mut_ptr();
let revprops_hash = unsafe {
let hash = apr_sys::apr_hash_make(pool_ptr);
let log_key = apr_sys::apr_pstrdup(pool_ptr, c"svn:log".as_ptr());
let log_value = subversion_sys::svn_string_ncreate(
log_msg_cstr.as_ptr() as *const std::os::raw::c_char,
log_msg.len(),
pool_ptr,
);
apr_sys::apr_hash_set(
hash,
log_key as *const std::ffi::c_void,
apr_sys::APR_HASH_KEY_STRING as isize,
log_value as *mut std::ffi::c_void,
);
if let Some(ref author_c) = author_cstr {
let author_key = apr_sys::apr_pstrdup(pool_ptr, c"svn:author".as_ptr());
let author_value = subversion_sys::svn_string_create(author_c.as_ptr(), pool_ptr);
apr_sys::apr_hash_set(
hash,
author_key as *const std::ffi::c_void,
apr_sys::APR_HASH_KEY_STRING as isize,
author_value as *mut std::ffi::c_void,
);
}
if let Some(revprops) = revprops {
for (key, value) in revprops {
let key_cstr = std::ffi::CString::new(key.as_str())?;
let pool_key = apr_sys::apr_pstrdup(pool_ptr, key_cstr.as_ptr());
let svn_string = subversion_sys::svn_string_ncreate(
value.as_ptr() as *const std::os::raw::c_char,
value.len(),
pool_ptr,
);
apr_sys::apr_hash_set(
hash,
pool_key as *const std::ffi::c_void,
apr_sys::APR_HASH_KEY_STRING as isize,
svn_string as *mut std::ffi::c_void,
);
}
}
hash
};
let commit_baton = commit_callback
.map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
let has_authz = authz_callback.is_some();
let authz_baton = authz_callback
.map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
let mut editor_ptr = std::ptr::null();
let mut edit_baton = std::ptr::null_mut();
extern "C" fn wrap_authz_callback(
required: subversion_sys::svn_repos_authz_access_t,
allowed: *mut i32,
root: *mut subversion_sys::svn_fs_root_t,
path: *const i8,
baton: *mut std::ffi::c_void,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || allowed.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&mut *(baton
as *mut Box<
dyn FnMut(
AuthzAccess,
&crate::fs::Root,
&str,
) -> Result<bool, Error<'static>>,
>)
};
let path_str = unsafe {
if path.is_null() {
""
} else {
std::ffi::CStr::from_ptr(path).to_str().unwrap_or("")
}
};
let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };
let access = AuthzAccess::from_raw(required);
match callback(access, &fs_root, path_str) {
Ok(is_allowed) => {
unsafe {
*allowed = if is_allowed { 1 } else { 0 };
}
std::ptr::null_mut()
}
Err(mut e) => unsafe { e.detach() },
}
}
extern "C" fn wrap_commit_callback(
commit_info: *const subversion_sys::svn_commit_info_t,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || commit_info.is_null() {
return std::ptr::null_mut();
}
let callback =
unsafe { &*(baton as *const Box<dyn Fn(crate::Revnum, &str, &std::ffi::CStr)>) };
let info = unsafe { &*commit_info };
let date_str = if !info.date.is_null() {
unsafe { std::ffi::CStr::from_ptr(info.date).to_string_lossy() }
} else {
std::borrow::Cow::Borrowed("")
};
let author_cstr = if !info.author.is_null() {
unsafe { std::ffi::CStr::from_ptr(info.author) }
} else {
c""
};
callback(
crate::Revnum::from(info.revision as u64),
&date_str,
author_cstr,
);
std::ptr::null_mut()
}
let ret = unsafe {
subversion_sys::svn_repos_get_commit_editor5(
&mut editor_ptr,
&mut edit_baton,
self.ptr,
txn.map(|t| t.as_ptr()).unwrap_or(std::ptr::null_mut()),
repos_url_cstr.as_ptr(),
base_path_cstr.as_ptr(),
revprops_hash,
if commit_callback.is_some() {
Some(wrap_commit_callback)
} else {
None
},
commit_baton,
if has_authz {
Some(wrap_authz_callback)
} else {
None
},
authz_baton,
pool_ptr,
)
};
if !ret.is_null() {
if !commit_baton.is_null() {
unsafe {
let _ = Box::from_raw(
commit_baton as *mut Box<dyn Fn(crate::Revnum, &str, &std::ffi::CStr)>,
);
}
}
if !authz_baton.is_null() {
unsafe {
let _ = Box::from_raw(
authz_baton
as *mut Box<
dyn FnMut(
AuthzAccess,
&crate::fs::Root,
&str,
)
-> Result<bool, Error<'static>>,
>,
);
}
}
}
Error::from_raw(ret)?;
let mut batons = Vec::new();
if !authz_baton.is_null() {
batons.push((
authz_baton,
drop_authz_commit_baton as crate::delta::DropperFn,
));
}
let editor = crate::delta::WrapEditor {
editor: editor_ptr,
baton: edit_baton,
_pool: apr::PoolHandle::owned(*pool),
callback_batons: batons,
};
Ok(editor)
}
pub unsafe fn begin_report(
&self,
revnum: Revnum,
fs_base: &str,
target: &str, tgt_path: Option<&str>, text_deltas: bool,
depth: crate::Depth,
ignore_ancestry: bool,
send_copyfrom_args: bool,
editor: *const subversion_sys::svn_delta_editor_t,
editor_baton: *mut std::ffi::c_void,
authz_read_func: Option<
Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>,
>,
) -> Result<Report, Error<'static>> {
let fs_base_cstr = std::ffi::CString::new(fs_base).unwrap();
let target_cstr = std::ffi::CString::new(target).unwrap();
let tgt_path_cstr = tgt_path.map(|t| std::ffi::CString::new(t).unwrap());
let pool = apr::Pool::new();
let mut report_baton: *mut std::ffi::c_void = std::ptr::null_mut();
let has_authz = authz_read_func.is_some();
let authz_baton = authz_read_func
.map(|callback| Box::into_raw(Box::new(callback)) as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
extern "C" fn wrap_authz_read_func(
allowed: *mut i32,
root: *mut subversion_sys::svn_fs_root_t,
path: *const i8,
baton: *mut std::ffi::c_void,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || allowed.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&mut *(baton
as *mut Box<dyn FnMut(&crate::fs::Root, &str) -> Result<bool, Error<'static>>>)
};
let path_str = unsafe {
if path.is_null() {
""
} else {
std::ffi::CStr::from_ptr(path).to_str().unwrap_or("")
}
};
let fs_root = unsafe { crate::fs::Root::from_raw(root, pool) };
match callback(&fs_root, path_str) {
Ok(is_allowed) => {
unsafe {
*allowed = if is_allowed { 1 } else { 0 };
}
std::ptr::null_mut()
}
Err(mut e) => unsafe { e.detach() },
}
}
let ret = unsafe {
subversion_sys::svn_repos_begin_report3(
&mut report_baton,
revnum.into(),
self.ptr,
fs_base_cstr.as_ptr(),
target_cstr.as_ptr(),
tgt_path_cstr
.as_ref()
.map(|t| t.as_ptr())
.unwrap_or(std::ptr::null()),
text_deltas.into(),
depth.into(),
ignore_ancestry.into(),
send_copyfrom_args.into(),
editor,
editor_baton,
if has_authz {
Some(wrap_authz_read_func)
} else {
None
},
authz_baton,
0, pool.as_mut_ptr(),
)
};
svn_result(ret)?;
let mut batons = Vec::new();
if !authz_baton.is_null() {
batons.push((
authz_baton,
drop_authz_read_baton as crate::delta::DropperFn,
));
}
Ok(Report {
baton: report_baton,
pool,
callback_batons: batons,
})
}
}
pub struct Report {
baton: *mut std::ffi::c_void,
pool: apr::Pool<'static>,
callback_batons: Vec<(*mut std::ffi::c_void, crate::delta::DropperFn)>,
}
impl Drop for Report {
fn drop(&mut self) {
for (baton, dropper) in &self.callback_batons {
if !baton.is_null() {
unsafe {
dropper(*baton);
}
}
}
self.callback_batons.clear();
}
}
impl Report {
pub fn set_path(
&self,
path: &str,
revision: Revnum,
depth: crate::Depth,
start_empty: bool,
lock_token: Option<&str>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let lock_token_cstr = lock_token.map(|t| std::ffi::CString::new(t).unwrap());
with_tmp_pool(|pool| {
let ret = unsafe {
subversion_sys::svn_repos_set_path3(
self.baton,
path_cstr.as_ptr(),
revision.into(),
depth.into(),
start_empty.into(),
lock_token_cstr
.as_ref()
.map(|t| t.as_ptr())
.unwrap_or(std::ptr::null()),
pool.as_mut_ptr(),
)
};
svn_result(ret)
})
}
pub fn link_path(
&self,
path: &str,
link_path: &str,
revision: Revnum,
depth: crate::Depth,
start_empty: bool,
lock_token: Option<&str>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
let link_path_cstr = std::ffi::CString::new(link_path).unwrap();
let lock_token_cstr = lock_token.map(|t| std::ffi::CString::new(t).unwrap());
with_tmp_pool(|pool| {
let ret = unsafe {
subversion_sys::svn_repos_link_path3(
self.baton,
path_cstr.as_ptr(),
link_path_cstr.as_ptr(),
revision.into(),
depth.into(),
start_empty.into(),
lock_token_cstr
.as_ref()
.map(|t| t.as_ptr())
.unwrap_or(std::ptr::null()),
pool.as_mut_ptr(),
)
};
svn_result(ret)
})
}
pub fn delete_path(&self, path: &str) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path).unwrap();
with_tmp_pool(|pool| {
let ret = unsafe {
subversion_sys::svn_repos_delete_path(
self.baton,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(ret)
})
}
pub fn finish(self) -> Result<(), Error<'static>> {
let pool_ptr = self.pool.as_mut_ptr();
let ret = unsafe { subversion_sys::svn_repos_finish_report(self.baton, pool_ptr) };
svn_result(ret)
}
pub fn abort(self) -> Result<(), Error<'static>> {
let pool_ptr = self.pool.as_mut_ptr();
let ret = unsafe { subversion_sys::svn_repos_abort_report(self.baton, pool_ptr) };
svn_result(ret)
}
}
impl Repos {
pub fn hooks_setenv(&mut self, hooks_env_path: &str) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = CString::new(hooks_env_path).unwrap();
let ret = unsafe {
subversion_sys::svn_repos_hooks_setenv(self.ptr, path_cstr.as_ptr(), pool.as_mut_ptr())
};
svn_result(ret)
}
pub fn start_commit_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_start_commit_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn pre_commit_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_pre_commit_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn post_commit_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_post_commit_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn pre_revprop_change_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr = unsafe {
subversion_sys::svn_repos_pre_revprop_change_hook(self.ptr, pool.as_mut_ptr())
};
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn post_revprop_change_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr = unsafe {
subversion_sys::svn_repos_post_revprop_change_hook(self.ptr, pool.as_mut_ptr())
};
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn pre_lock_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_pre_lock_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn post_lock_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_post_lock_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn pre_unlock_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_pre_unlock_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn post_unlock_hook_path(&self) -> std::path::PathBuf {
let pool = apr::Pool::new();
let path_ptr =
unsafe { subversion_sys::svn_repos_post_unlock_hook(self.ptr, pool.as_mut_ptr()) };
let path = unsafe { std::ffi::CStr::from_ptr(path_ptr) }
.to_string_lossy()
.into_owned();
std::path::PathBuf::from(path)
}
pub fn begin_txn_for_update(
&self,
revnum: Revnum,
author: &str,
) -> Result<crate::fs::Transaction<'_>, Error<'static>> {
let pool = apr::Pool::new();
let mut txn_ptr = std::ptr::null_mut();
let author_cstr = std::ffi::CString::new(author)?;
let ret = unsafe {
subversion_sys::svn_repos_fs_begin_txn_for_update(
&mut txn_ptr,
self.ptr,
revnum.0,
author_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
Ok(unsafe { crate::fs::Transaction::from_ptr_and_pool(txn_ptr, pool) })
}
pub fn check_revision_access(
&self,
revision: Revnum,
authz: Option<&Authz>,
_user: Option<&str>,
) -> Result<RevisionAccessLevel, Error<'static>> {
let pool = apr::Pool::new();
if authz.is_none() {
return Ok(RevisionAccessLevel::Full);
}
let authz = authz.unwrap();
extern "C" fn authz_read_func(
allowed: *mut subversion_sys::svn_boolean_t,
_root: *mut subversion_sys::svn_fs_root_t,
path: *const std::os::raw::c_char,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let authz = unsafe { &*(baton as *const Authz) };
let path_str = unsafe { std::ffi::CStr::from_ptr(path) }.to_str().unwrap();
match authz.check_access(None, path_str, None, AuthzAccess::Read) {
Ok(access) => {
unsafe { *allowed = if access { 1 } else { 0 } };
std::ptr::null_mut()
}
Err(e) => unsafe { e.into_raw() },
}
}
let mut access_level: subversion_sys::svn_repos_revision_access_level_t = 0;
let ret = unsafe {
subversion_sys::svn_repos_check_revision_access(
&mut access_level,
self.ptr,
revision.0,
Some(authz_read_func),
authz as *const Authz as *mut std::ffi::c_void,
pool.as_mut_ptr(),
)
};
svn_result(ret)?;
Ok(RevisionAccessLevel::from(access_level))
}
pub fn dated_revision(&self, tm: apr::apr_time_t) -> Result<Revnum, Error<'static>> {
with_tmp_pool(|pool| {
let mut revision: subversion_sys::svn_revnum_t = -1; let err = unsafe {
subversion_sys::svn_repos_dated_revision(
&mut revision,
self.ptr,
tm,
pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(Revnum(revision))
})
}
pub fn deleted_rev(
&self,
path: &str,
start: Revnum,
end: Revnum,
) -> Result<Option<Revnum>, Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
let fs_ptr = unsafe { subversion_sys::svn_repos_fs(self.ptr) };
with_tmp_pool(|pool| {
let mut deleted: subversion_sys::svn_revnum_t = -1; let err = unsafe {
subversion_sys::svn_repos_deleted_rev(
fs_ptr,
path_cstr.as_ptr(),
start.0,
end.0,
&mut deleted,
pool.as_mut_ptr(),
)
};
svn_result(err)?;
if deleted < 0 {
Ok(None)
} else {
Ok(Some(Revnum(deleted)))
}
})
}
pub fn history(
&self,
path: &str,
start: Revnum,
end: Revnum,
cross_copies: bool,
history_func: &mut dyn FnMut(&str, Revnum) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
let fs_ptr = unsafe { subversion_sys::svn_repos_fs(self.ptr) };
struct Baton<'a> {
func: &'a mut dyn FnMut(&str, Revnum) -> Result<(), Error<'static>>,
error: Option<Error<'static>>,
}
unsafe extern "C" fn history_trampoline(
baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
revision: subversion_sys::svn_revnum_t,
_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("");
match (baton.func)(path_str, Revnum(revision)) {
Ok(()) => std::ptr::null_mut(),
Err(e) => {
baton.error = Some(e);
subversion_sys::svn_error_create(
subversion_sys::svn_errno_t_SVN_ERR_CEASE_INVOCATION as i32,
std::ptr::null_mut(),
std::ptr::null(),
)
}
}
}
let mut baton = Baton {
func: history_func,
error: None,
};
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_repos_history2(
fs_ptr,
path_cstr.as_ptr(),
Some(history_trampoline),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
None, std::ptr::null_mut(), start.0,
end.0,
cross_copies as i32,
pool.as_mut_ptr(),
)
};
if let Some(e) = baton.error.take() {
return Err(e);
}
svn_result(err)
})
}
pub fn get_logs(
&self,
paths: &[&str],
start: Revnum,
end: Revnum,
limit: usize,
discover_changed_paths: bool,
strict_node_history: bool,
include_merged_revisions: bool,
revprops: &[&str],
log_receiver: &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path_cstrings: Vec<std::ffi::CString> = paths
.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect();
let mut paths_array =
apr::tables::TypedArray::<*const std::os::raw::c_char>::new(&pool, paths.len() as i32);
for cstr in &path_cstrings {
paths_array.push(cstr.as_ptr());
}
let revprop_cstrings: Vec<std::ffi::CString> = revprops
.iter()
.map(|p| std::ffi::CString::new(*p).unwrap())
.collect();
let mut revprops_array = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
&pool,
revprops.len() as i32,
);
for cstr in &revprop_cstrings {
revprops_array.push(cstr.as_ptr());
}
let baton = Box::into_raw(Box::new(log_receiver)) as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_repos_get_logs4(
self.ptr,
paths_array.as_ptr(),
start.0,
end.0,
limit as i32,
discover_changed_paths as i32,
strict_node_history as i32,
include_merged_revisions as i32,
revprops_array.as_ptr(),
None, std::ptr::null_mut(), Some(crate::wrap_log_entry_receiver),
baton,
pool.as_mut_ptr(),
)
};
let _ = unsafe {
Box::from_raw(
baton as *mut &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
)
};
svn_result(err)
}
pub fn trace_node_locations(
&self,
fs: &mut crate::fs::Fs,
fs_path: &str,
peg_revision: Revnum,
location_revisions: &[Revnum],
) -> Result<std::collections::HashMap<Revnum, String>, Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = std::ffi::CString::new(fs_path).unwrap();
let mut revs_array = apr::tables::TypedArray::<subversion_sys::svn_revnum_t>::new(
&pool,
location_revisions.len() as i32,
);
for rev in location_revisions {
revs_array.push(rev.0);
}
let revs_arr_ptr: *const apr_sys::apr_array_header_t = unsafe { revs_array.as_ptr() };
let mut locations: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_repos_trace_node_locations(
fs.as_mut_ptr(),
&mut locations,
path_cstr.as_ptr(),
peg_revision.0,
revs_arr_ptr,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
)
};
svn_result(err)?;
let mut result = std::collections::HashMap::new();
if !locations.is_null() {
unsafe {
let mut hi = apr_sys::apr_hash_first(pool.as_mut_ptr(), locations);
while !hi.is_null() {
let mut key_ptr: *const std::ffi::c_void = std::ptr::null();
let mut val_ptr: *mut std::ffi::c_void = std::ptr::null_mut();
apr_sys::apr_hash_this(hi, &mut key_ptr, std::ptr::null_mut(), &mut val_ptr);
if !key_ptr.is_null() && !val_ptr.is_null() {
let rev = *(key_ptr as *const subversion_sys::svn_revnum_t);
let path = std::ffi::CStr::from_ptr(val_ptr as *const i8)
.to_string_lossy()
.into_owned();
result.insert(Revnum(rev), path);
}
hi = apr_sys::apr_hash_next(hi);
}
}
}
Ok(result)
}
pub fn get_committed_info(
root: &mut crate::fs::Root,
path: &str,
) -> Result<(Revnum, Option<String>, Option<String>), Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = std::ffi::CString::new(path)?;
let mut committed_rev: subversion_sys::svn_revnum_t = -1;
let mut committed_date: *const i8 = std::ptr::null();
let mut last_author: *const i8 = std::ptr::null();
let err = unsafe {
subversion_sys::svn_repos_get_committed_info(
&mut committed_rev,
&mut committed_date,
&mut last_author,
root.as_mut_ptr(),
path_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)?;
let date = if committed_date.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(committed_date) }
.to_string_lossy()
.into_owned(),
)
};
let author = if last_author.is_null() {
None
} else {
Some(
unsafe { std::ffi::CStr::from_ptr(last_author) }
.to_string_lossy()
.into_owned(),
)
};
Ok((Revnum(committed_rev), date, author))
}
pub fn get_file_revs(
&self,
path: &str,
start: Revnum,
end: Revnum,
include_merged_revisions: bool,
mut handler: impl FnMut(
&str,
Revnum,
&std::collections::HashMap<String, Vec<u8>>,
bool,
&std::collections::HashMap<String, Vec<u8>>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
struct Baton<'a> {
func: &'a mut dyn FnMut(
&str,
Revnum,
&std::collections::HashMap<String, Vec<u8>>,
bool,
&std::collections::HashMap<String, Vec<u8>>,
) -> Result<(), Error<'static>>,
}
unsafe extern "C" fn file_rev_trampoline(
baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
rev: subversion_sys::svn_revnum_t,
rev_props: *mut apr_sys::apr_hash_t,
result_of_merge: subversion_sys::svn_boolean_t,
txdelta_handler: *mut subversion_sys::svn_txdelta_window_handler_t,
txdelta_baton: *mut *mut std::ffi::c_void,
prop_diffs: *mut apr_sys::apr_array_header_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = &mut *(baton as *mut Baton<'_>);
let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
let rev_props_map = if rev_props.is_null() {
std::collections::HashMap::new()
} else {
let prop_hash = crate::props::PropHash::from_ptr(rev_props);
prop_hash.to_hashmap()
};
let prop_diffs_map = if prop_diffs.is_null() {
std::collections::HashMap::new()
} else {
let array = std::slice::from_raw_parts(
(*prop_diffs).elts as *const subversion_sys::svn_prop_t,
(*prop_diffs).nelts as usize,
);
let mut props = std::collections::HashMap::new();
for prop in array {
let name = std::ffi::CStr::from_ptr(prop.name).to_str().unwrap_or("");
let value = if prop.value.is_null() {
Vec::new()
} else {
let s = &*prop.value;
std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec()
};
props.insert(name.to_string(), value);
}
props
};
if !txdelta_handler.is_null() {
*txdelta_handler = None;
}
if !txdelta_baton.is_null() {
*txdelta_baton = std::ptr::null_mut();
}
match (baton.func)(
path_str,
Revnum(rev),
&rev_props_map,
result_of_merge != 0,
&prop_diffs_map,
) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
let mut baton = Baton { func: &mut handler };
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_repos_get_file_revs2(
self.ptr,
path_cstr.as_ptr(),
start.0,
end.0,
include_merged_revisions as i32,
None,
std::ptr::null_mut(),
Some(file_rev_trampoline),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn stat(
&self,
root: &crate::fs::Root,
path: &str,
) -> Result<Option<RepoDirEntry>, Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
with_tmp_pool(|pool| {
let mut dirent: *mut subversion_sys::svn_dirent_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_repos_stat(
&mut dirent,
root.as_ptr() as *mut _,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)?;
if dirent.is_null() {
return Ok(None);
}
Ok(Some(unsafe { RepoDirEntry::from_raw(dirent) }))
})
}
pub fn info_format(&self) -> Result<(i32, (i32, i32, i32)), Error<'static>> {
with_tmp_pool(|pool| {
let mut repos_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_repos_info_format(
&mut repos_format,
&mut supports_version,
self.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((repos_format as i32, version))
})
}
pub fn node_location_segments(
&self,
path: &str,
peg_revision: Revnum,
start_rev: Revnum,
end_rev: Revnum,
mut receiver: impl FnMut(Option<&str>, Revnum, Revnum) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
struct Baton<'a> {
func: &'a mut dyn FnMut(Option<&str>, Revnum, Revnum) -> Result<(), Error<'static>>,
}
unsafe extern "C" fn segment_trampoline(
segment: *mut subversion_sys::svn_location_segment_t,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = &mut *(baton as *mut Baton<'_>);
if segment.is_null() {
return std::ptr::null_mut();
}
let seg = &*segment;
let path_opt = if seg.path.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(seg.path).to_str().unwrap_or(""))
};
match (baton.func)(path_opt, Revnum(seg.range_start), Revnum(seg.range_end)) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
let mut baton = Baton {
func: &mut receiver,
};
with_tmp_pool(|pool| {
let err = unsafe {
subversion_sys::svn_repos_node_location_segments(
self.ptr,
path_cstr.as_ptr(),
peg_revision.0,
start_rev.0,
end_rev.0,
Some(segment_trampoline),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
None,
std::ptr::null_mut(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn fs_get_mergeinfo(
&self,
paths: &[&str],
revision: Revnum,
inherit: crate::mergeinfo::MergeinfoInheritance,
include_descendants: 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_repos_fs_get_mergeinfo2(
self.ptr,
arr.as_ptr(),
revision.0,
inherit.into(),
if include_descendants { 1 } else { 0 },
None, std::ptr::null_mut(), Some(mergeinfo_trampoline),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
}
pub struct RepoDirEntry {
kind: crate::NodeKind,
size: i64,
has_props: bool,
created_rev: crate::Revnum,
time: apr::apr_time_t,
last_author: Option<String>,
}
impl RepoDirEntry {
unsafe fn from_raw(ptr: *const subversion_sys::svn_dirent_t) -> Self {
let d = &*ptr;
Self {
kind: d.kind.into(),
size: d.size,
has_props: d.has_props != 0,
created_rev: crate::Revnum(d.created_rev),
time: d.time,
last_author: if d.last_author.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(d.last_author)
.to_string_lossy()
.into_owned(),
)
},
}
}
pub fn kind(&self) -> crate::NodeKind {
self.kind
}
pub fn size(&self) -> i64 {
self.size
}
pub fn has_props(&self) -> bool {
self.has_props
}
pub fn created_rev(&self) -> crate::Revnum {
self.created_rev
}
pub fn time(&self) -> apr::apr_time_t {
self.time
}
pub fn last_author(&self) -> Option<&str> {
self.last_author.as_deref()
}
}
pub fn list(
root: &crate::fs::Root,
path: &str,
patterns: &[&str],
depth: crate::Depth,
path_info_only: bool,
mut receiver: impl FnMut(&str, RepoDirEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
struct Baton<'a> {
func: &'a mut dyn FnMut(&str, RepoDirEntry) -> Result<(), Error<'static>>,
}
unsafe extern "C" fn list_trampoline(
path: *const std::os::raw::c_char,
dirent: *mut subversion_sys::svn_dirent_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 entry = RepoDirEntry::from_raw(dirent);
match (baton.func)(path_str, entry) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
let mut baton = Baton {
func: &mut receiver,
};
with_tmp_pool(|pool| {
let pattern_cstrings: Vec<std::ffi::CString> = patterns
.iter()
.map(|p| std::ffi::CString::new(*p))
.collect::<Result<_, _>>()?;
let patterns_ptr: *const apr_sys::apr_array_header_t = if pattern_cstrings.is_empty() {
std::ptr::null()
} else {
let mut arr = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
pool,
pattern_cstrings.len() as i32,
);
for cstr in &pattern_cstrings {
arr.push(cstr.as_ptr());
}
unsafe { arr.as_ptr() }
};
let err = unsafe {
subversion_sys::svn_repos_list(
root.as_ptr() as *mut _,
path_cstr.as_ptr(),
patterns_ptr,
depth.into(),
path_info_only as i32,
None,
std::ptr::null_mut(),
Some(list_trampoline),
&mut baton as *mut Baton<'_> as *mut std::ffi::c_void,
None,
std::ptr::null_mut(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
#[cfg(test)]
mod additional_tests {
use super::*;
use std::path::Path;
#[test]
fn test_fs_pack_invalid_path() {
let invalid_path = Path::new("/non/existent/repo");
let result = fs_pack(invalid_path, None, None);
assert!(result.is_err());
}
#[test]
#[cfg(feature = "delta")]
fn test_get_commit_editor_basic() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let repo_url = crate::path_to_file_url(&repo_path);
let result = repo.get_commit_editor(
&repo_url,
"", "Test commit message",
Some("test_author"),
None, None, None, None, );
result.unwrap();
}
#[test]
fn test_authz_basic() {
let temp_dir = tempfile::tempdir().unwrap();
let authz_path = temp_dir.path().join("authz");
std::fs::write(&authz_path, "[/]\n* = rw\n").unwrap();
let authz = Authz::read(&authz_path, None, true).unwrap();
let has_access = authz
.check_access(Some("testrepo"), "/", Some("testuser"), AuthzAccess::Read)
.unwrap();
assert!(
has_access,
"Should have read access with wildcard permission"
);
}
#[test]
fn test_authz_no_access() {
let temp_dir = tempfile::tempdir().unwrap();
let authz_path = temp_dir.path().join("authz");
std::fs::write(&authz_path, "").unwrap();
let authz = Authz::read(&authz_path, None, true).unwrap();
let has_access = authz
.check_access(Some("testrepo"), "/", Some("testuser"), AuthzAccess::Read)
.unwrap();
assert!(!has_access, "Should have no access with empty authz");
}
#[test]
fn test_authz_parse() {
let authz_content = "[/]\n* = r\n\n[/trunk]\ntestuser = rw\n";
let authz = Authz::parse(authz_content, None).unwrap();
assert!(
authz
.check_access(None, "/", Some("anyone"), AuthzAccess::Read)
.unwrap(),
"Should have read access to root"
);
assert!(
authz
.check_access(None, "/trunk", Some("testuser"), AuthzAccess::Write)
.unwrap(),
"testuser should have write access to /trunk"
);
assert!(
!authz
.check_access(None, "/trunk", Some("otheruser"), AuthzAccess::Write)
.unwrap(),
"otheruser should not have write access to /trunk"
);
}
#[test]
fn test_authz_parse_with_groups() {
let groups_content = "[groups]\nadmins = alice, bob\n";
let authz_content = "[/]\n@admins = rw\n* = r\n";
let authz = Authz::parse(authz_content, Some(groups_content)).unwrap();
assert!(
authz
.check_access(None, "/", Some("alice"), AuthzAccess::Write)
.unwrap(),
"alice (admin) should have write access"
);
assert!(
authz
.check_access(None, "/", Some("bob"), AuthzAccess::Write)
.unwrap(),
"bob (admin) should have write access"
);
assert!(
!authz
.check_access(None, "/", Some("charlie"), AuthzAccess::Write)
.unwrap(),
"charlie should not have write access"
);
}
#[test]
fn test_authz_parse_complex_rules() {
let authz_content = r#"[/]
* = r
[/trunk]
dev-team = rw
* = r
[/trunk/secret]
admin = rw
* =
[/branches]
* = rw
"#;
let authz = Authz::parse(authz_content, None).unwrap();
assert!(authz
.check_access(None, "/", Some("anyone"), AuthzAccess::Read)
.unwrap());
assert!(authz
.check_access(None, "/trunk", Some("dev-team"), AuthzAccess::ReadWrite)
.unwrap());
assert!(authz
.check_access(None, "/trunk", Some("other"), AuthzAccess::Read)
.unwrap());
assert!(!authz
.check_access(None, "/trunk", Some("other"), AuthzAccess::Write)
.unwrap());
assert!(authz
.check_access(None, "/trunk/secret", Some("admin"), AuthzAccess::ReadWrite)
.unwrap());
assert!(!authz
.check_access(None, "/trunk/secret", Some("other"), AuthzAccess::Read)
.unwrap());
assert!(authz
.check_access(None, "/branches", Some("anyone"), AuthzAccess::ReadWrite)
.unwrap());
}
#[test]
fn test_authz_parse_invalid() {
let invalid_authz = "this is not valid authz format";
let result = Authz::parse(invalid_authz, None);
assert!(result.is_err(), "Should fail to parse invalid authz");
}
#[test]
fn test_check_revision_access() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let authz_content = "[/trunk]\n* = r\n\n[/secret]\n* =\n";
let authz = Authz::parse(authz_content, None).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(Revnum(0), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_dir("/secret").unwrap();
root.make_file("/trunk/file.txt").unwrap();
root.make_file("/secret/password.txt").unwrap();
txn.commit().unwrap();
let access = repo.check_revision_access(Revnum(1), None, None).unwrap();
assert_eq!(access, RevisionAccessLevel::Full);
let access = repo
.check_revision_access(Revnum(1), Some(&authz), Some("user"))
.unwrap();
assert_eq!(access, RevisionAccessLevel::Partial);
let mut txn = fs.begin_txn(Revnum(1), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/trunk/another.txt").unwrap();
txn.commit().unwrap();
let access = repo
.check_revision_access(Revnum(2), Some(&authz), Some("user"))
.unwrap();
assert_eq!(access, RevisionAccessLevel::Full);
let mut txn = fs.begin_txn(Revnum(2), 0).unwrap();
let mut root = txn.root().unwrap();
root.make_file("/secret/key.txt").unwrap();
txn.commit().unwrap();
let access = repo
.check_revision_access(Revnum(3), Some(&authz), Some("user"))
.unwrap();
assert_eq!(access, RevisionAccessLevel::None);
}
#[test]
fn test_report_with_commit_editor() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let repo_url = crate::path_to_file_url(&repo_path);
let editor_result = repo.get_commit_editor(
&repo_url,
"/",
"Test commit for report testing",
Some("test_author"),
None,
None,
None, None, );
assert!(
editor_result.is_ok(),
"Failed to get commit editor: {:?}",
editor_result.err()
);
if let Ok(editor_box) = editor_result {
let (editor_ptr, baton_ptr) = editor_box.as_raw_parts();
let report_result = unsafe {
repo.begin_report(
crate::Revnum::from_raw(0).unwrap(),
"/",
"", None, false,
crate::Depth::Infinity,
false,
false,
editor_ptr,
baton_ptr,
None, )
};
assert!(
report_result.is_ok(),
"Failed to begin report: {:?}",
report_result.err()
);
if let Ok(report) = report_result {
let abort_result = report.abort();
assert!(
abort_result.is_ok(),
"Failed to abort report: {:?}",
abort_result.err()
);
}
}
}
#[test]
fn test_report_operations() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let repo_url = crate::path_to_file_url(&repo_path);
let editor_box = repo
.get_commit_editor(
&repo_url,
"/",
"Test commit for report operations",
Some("test_author"),
None,
None,
None, None, )
.unwrap();
let (editor_ptr, baton_ptr) = editor_box.as_raw_parts();
let report = unsafe {
repo.begin_report(
crate::Revnum::from_raw(0).unwrap(),
"/",
"", None, false,
crate::Depth::Infinity,
false,
false,
editor_ptr,
baton_ptr,
None, )
}
.unwrap();
report
.set_path(
"",
crate::Revnum::from_raw(0).unwrap(),
crate::Depth::Infinity,
false,
None,
)
.unwrap();
report.delete_path("nonexistent_path").unwrap();
report
.link_path(
"link_target",
"link_source",
crate::Revnum::from_raw(0).unwrap(),
crate::Depth::Files,
false,
None,
)
.unwrap();
report.abort().unwrap();
}
#[test]
fn test_hook_paths() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let start_commit_hook = repo.start_commit_hook_path();
assert!(start_commit_hook.to_str().unwrap().contains("start-commit"));
assert!(start_commit_hook.to_str().unwrap().contains("hooks"));
let pre_commit_hook = repo.pre_commit_hook_path();
assert!(pre_commit_hook.to_str().unwrap().contains("pre-commit"));
let post_commit_hook = repo.post_commit_hook_path();
assert!(post_commit_hook.to_str().unwrap().contains("post-commit"));
let pre_revprop_hook = repo.pre_revprop_change_hook_path();
assert!(pre_revprop_hook
.to_str()
.unwrap()
.contains("pre-revprop-change"));
let post_revprop_hook = repo.post_revprop_change_hook_path();
assert!(post_revprop_hook
.to_str()
.unwrap()
.contains("post-revprop-change"));
let pre_lock_hook = repo.pre_lock_hook_path();
assert!(pre_lock_hook.to_str().unwrap().contains("pre-lock"));
let post_lock_hook = repo.post_lock_hook_path();
assert!(post_lock_hook.to_str().unwrap().contains("post-lock"));
let pre_unlock_hook = repo.pre_unlock_hook_path();
assert!(pre_unlock_hook.to_str().unwrap().contains("pre-unlock"));
let post_unlock_hook = repo.post_unlock_hook_path();
assert!(post_unlock_hook.to_str().unwrap().contains("post-unlock"));
}
#[test]
fn test_hooks_setenv() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let mut repo = Repos::create(&repo_path).unwrap();
let hooks_env_path = temp_dir.path().join("hooks.env");
std::fs::write(&hooks_env_path, "TEST_VAR=test_value\n").unwrap();
repo.hooks_setenv(hooks_env_path.to_str().unwrap()).unwrap();
}
#[test]
fn test_dir_delta2() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum::from(0u32), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_dir("/trunk").unwrap();
txn_root.make_file("/trunk/test.txt").unwrap();
let mut stream = txn_root.apply_text("/trunk/test.txt", None).unwrap();
stream.write(b"Initial content\n").unwrap();
stream.close().unwrap();
txn.commit().unwrap();
let mut txn2 = fs.begin_txn(crate::Revnum::from(1u32), 0).unwrap();
let mut txn_root2 = txn2.root().unwrap();
let mut stream2 = txn_root2.apply_text("/trunk/test.txt", None).unwrap();
stream2.write(b"Modified content\n").unwrap();
stream2.close().unwrap();
txn_root2.make_file("/trunk/new.txt").unwrap();
let mut stream3 = txn_root2.apply_text("/trunk/new.txt", None).unwrap();
stream3.write(b"New file\n").unwrap();
stream3.close().unwrap();
txn2.commit().unwrap();
let root1 = fs.revision_root(crate::Revnum::from(1u32)).unwrap();
let root2 = fs.revision_root(crate::Revnum::from(2u32)).unwrap();
let pool = apr::Pool::new();
let editor = crate::delta::default_editor(pool);
let result = dir_delta2(
&root1,
"/trunk",
"",
&root2,
"/trunk",
&editor,
true, crate::Depth::Infinity,
false, false, );
result.unwrap();
}
#[test]
fn test_get_commit_editor_with_authz_callback() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let repo_url = crate::path_to_file_url(&repo_path);
let authz_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let authz_called_clone = authz_called.clone();
let result = repo.get_commit_editor(
&repo_url,
"",
"Test commit with authz",
Some("test_author"),
None,
None,
Some(Box::new(move |_access, _root, _path| {
authz_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(true) })),
None, );
assert!(
result.is_ok(),
"Failed to get commit editor with authz callback: {:?}",
result.err()
);
}
#[test]
fn test_begin_report_with_authz_callback() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let repo_url = crate::path_to_file_url(&repo_path);
let editor_result = repo.get_commit_editor(
&repo_url,
"",
"Test commit",
Some("test_author"),
None,
None,
None,
None, );
let editor_box = editor_result.unwrap();
{
let (editor_ptr, baton_ptr) = editor_box.as_raw_parts();
let authz_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let authz_called_clone = authz_called.clone();
let report_result = unsafe {
repo.begin_report(
crate::Revnum::from_raw(0).unwrap(),
"/",
"",
None,
false,
crate::Depth::Infinity,
false,
false,
editor_ptr,
baton_ptr,
Some(Box::new(move |_root, _path| {
authz_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(true) })),
)
};
let report = report_result.unwrap();
report.abort().unwrap();
}
}
#[test]
fn test_callback_cleanup() {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/test.txt").unwrap();
let mut stream = txn_root.apply_text("/test.txt", None).unwrap();
stream.write(b"test content").unwrap();
stream.close().unwrap();
txn.commit().unwrap();
let callback_called = Arc::new(AtomicBool::new(false));
let callback_called_clone = callback_called.clone();
{
let mut buffer = Vec::new();
let mut stream = crate::io::wrap_write(&mut buffer).unwrap();
let mut options = DumpOptions {
start_rev: Some(crate::Revnum(1)),
end_rev: Some(crate::Revnum(1)),
include_revprops: true,
include_changes: true,
filter_func: Some(Box::new(move |_root, _path| {
callback_called_clone.store(true, Ordering::SeqCst);
Ok(true)
})),
..Default::default()
};
repo.dump(&mut stream, &mut options).unwrap();
}
}
#[test]
fn test_get_commit_editor_with_txn_none() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let repo_url = crate::path_to_file_url(&repo_path);
let result = repo.get_commit_editor(
&repo_url,
"",
"Test commit with txn=None",
Some("test_author"),
None,
None,
None,
None, );
assert!(
result.is_ok(),
"get_commit_editor with txn=None should succeed: {:?}",
result.err()
);
}
#[test]
fn test_commit_txn_creates_revision() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let txn = repo
.begin_txn_for_commit(
crate::Revnum(0),
"test_author",
"Test commit via commit_txn",
)
.unwrap();
let (new_rev, conflict) = repo.commit_txn(txn).unwrap();
assert_eq!(new_rev, crate::Revnum(1));
assert_eq!(conflict, None);
}
#[test]
fn test_commit_txn_with_changes() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "test_author", "Add trunk directory")
.unwrap();
{
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
}
let (new_rev, conflict) = repo.commit_txn(txn).unwrap();
assert_eq!(new_rev, crate::Revnum(1));
assert_eq!(conflict, None);
let fs = repo.fs().unwrap();
let rev_root = fs.revision_root(crate::Revnum(1)).unwrap();
let kind = rev_root.check_path("/trunk").unwrap();
assert_eq!(kind, crate::NodeKind::Dir);
}
#[test]
fn test_dated_revision_returns_rev0() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let rev = repo.dated_revision(1).unwrap();
assert_eq!(rev, crate::Revnum(0));
}
#[test]
fn test_deleted_rev_finds_deletion() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add foo")
.unwrap();
txn.root().unwrap().make_file("/foo").unwrap();
repo.commit_txn(txn).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(1), "author", "delete foo")
.unwrap();
txn.root().unwrap().delete("/foo").unwrap();
repo.commit_txn(txn).unwrap();
let deleted = repo
.deleted_rev("/foo", crate::Revnum(1), crate::Revnum(2))
.unwrap();
assert_eq!(deleted, Some(crate::Revnum(2)));
}
#[test]
fn test_deleted_rev_returns_none_when_not_deleted() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add foo")
.unwrap();
txn.root().unwrap().make_file("/foo").unwrap();
repo.commit_txn(txn).unwrap();
let deleted = repo
.deleted_rev("/foo", crate::Revnum(1), crate::Revnum(1))
.unwrap();
assert_eq!(deleted, None);
}
#[test]
fn test_history_collects_revisions() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add bar")
.unwrap();
txn.root().unwrap().make_file("/bar").unwrap();
repo.commit_txn(txn).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(1), "author", "tag bar")
.unwrap();
txn.root()
.unwrap()
.change_node_prop("/bar", "test:prop", b"value")
.unwrap();
repo.commit_txn(txn).unwrap();
let mut entries: Vec<(String, crate::Revnum)> = Vec::new();
repo.history(
"/bar",
crate::Revnum(1),
crate::Revnum(2),
false,
&mut |path, rev| {
entries.push((path.to_string(), rev));
Ok(())
},
)
.unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0], ("/bar".to_string(), crate::Revnum(2)));
assert_eq!(entries[1], ("/bar".to_string(), crate::Revnum(1)));
}
#[test]
fn test_history_callback_error_propagates() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add baz")
.unwrap();
txn.root().unwrap().make_file("/baz").unwrap();
repo.commit_txn(txn).unwrap();
let result = repo.history(
"/baz",
crate::Revnum(0),
crate::Revnum(1),
false,
&mut |_path, _rev| Err(crate::Error::from_message("test error")),
);
assert!(result.is_err());
}
#[test]
fn test_get_logs_retrieves_entries() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "alice", "initial commit")
.unwrap();
txn.root().unwrap().make_file("/file.txt").unwrap();
repo.commit_txn(txn).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(1), "bob", "add other")
.unwrap();
txn.root().unwrap().make_file("/other.txt").unwrap();
repo.commit_txn(txn).unwrap();
let mut revisions: Vec<crate::Revnum> = Vec::new();
repo.get_logs(
&[],
crate::Revnum(1),
crate::Revnum(2),
0, false, false, false, &["svn:log", "svn:author"],
&mut |entry| {
if let Some(rev) = entry.revision() {
revisions.push(rev);
}
Ok(())
},
)
.unwrap();
assert_eq!(revisions.len(), 2);
assert_eq!(revisions[0], crate::Revnum(1));
assert_eq!(revisions[1], crate::Revnum(2));
}
#[test]
fn test_get_logs_with_limit() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let files = ["/file0.txt", "/file1.txt", "/file2.txt"];
for (i, path) in files.iter().enumerate() {
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(i as _), "author", "commit")
.unwrap();
txn.root().unwrap().make_file(*path).unwrap();
repo.commit_txn(txn).unwrap();
}
let mut count = 0usize;
repo.get_logs(
&[],
crate::Revnum(1),
crate::Revnum(3),
2, false,
false,
false,
&[],
&mut |_entry| {
count += 1;
Ok(())
},
)
.unwrap();
assert_eq!(count, 2);
}
#[test]
fn test_trace_node_locations_tracks_file() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "alice", "add file")
.unwrap();
txn.root().unwrap().make_file("/file.txt").unwrap();
repo.commit_txn(txn).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(1), "bob", "add other")
.unwrap();
txn.root().unwrap().make_file("/other.txt").unwrap();
repo.commit_txn(txn).unwrap();
let mut fs = repo.fs().expect("repos must have a filesystem");
let locations = repo
.trace_node_locations(
&mut fs,
"/file.txt",
crate::Revnum(1),
&[crate::Revnum(1), crate::Revnum(2)],
)
.unwrap();
assert_eq!(
locations.get(&crate::Revnum(1)).map(|s| s.as_str()),
Some("/file.txt")
);
assert_eq!(
locations.get(&crate::Revnum(2)).map(|s| s.as_str()),
Some("/file.txt")
);
}
#[test]
fn test_fs_get_inherited_props_returns_empty_for_root() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "alice", "add file")
.unwrap();
let mut txn_root = txn.root().unwrap();
txn_root.make_file("/file.txt").unwrap();
drop(txn_root);
repo.commit_txn(txn).unwrap();
let fs = repo.fs().expect("repos must have a filesystem");
let mut root = fs.revision_root(crate::Revnum(1)).unwrap();
let inherited = repo
.fs_get_inherited_props(&mut root, "/file.txt", None)
.unwrap();
for (_path, props) in &inherited {
assert!(
props.is_empty(),
"no props expected on ancestors, got {props:?}"
);
}
}
#[test]
fn test_rev_prop_returns_value() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let date_val = repo.rev_prop(crate::Revnum(0), "svn:date").unwrap();
assert!(date_val.is_some(), "svn:date should be present on rev 0");
let date_bytes = date_val.unwrap();
assert!(
!date_bytes.is_empty(),
"svn:date should be a non-empty value"
);
let date_str = std::str::from_utf8(&date_bytes).unwrap();
assert!(
date_str.contains('T'),
"svn:date should contain 'T': {date_str}"
);
}
#[test]
fn test_rev_prop_returns_none_for_missing() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let val = repo.rev_prop(crate::Revnum(0), "svn:log").unwrap();
assert_eq!(val, None);
}
#[test]
fn test_stat_returns_dirent_for_existing_path() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/readme.txt").unwrap();
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let entry = repo.stat(&root, "/readme.txt").unwrap();
assert!(entry.is_some(), "stat should return Some for existing path");
let entry = entry.unwrap();
assert_eq!(entry.kind(), crate::NodeKind::File);
assert_eq!(entry.created_rev(), rev1);
}
#[test]
fn test_stat_returns_none_for_missing_path() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let root = fs.revision_root(crate::Revnum(0)).unwrap();
let entry = repo.stat(&root, "/no-such-path").unwrap();
assert_eq!(
entry.is_none(),
true,
"stat should return None for missing path"
);
}
#[test]
fn test_get_file_revs_collects_revisions() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
{
let mut root = txn.root().unwrap();
root.make_file("/hello.txt").unwrap();
root.set_file_contents("/hello.txt", b"hello\n").unwrap();
}
let rev1 = txn.commit().unwrap();
let mut txn2 = fs.begin_txn(rev1, 0).unwrap();
{
let mut root = txn2.root().unwrap();
root.set_file_contents("/hello.txt", b"hello world\n")
.unwrap();
}
let rev2 = txn2.commit().unwrap();
let mut seen_revs: Vec<crate::Revnum> = Vec::new();
repo.get_file_revs(
"/hello.txt",
crate::Revnum(1),
rev2,
false,
|_path, rev, _rev_props, _merged, _prop_diffs| {
seen_revs.push(rev);
Ok(())
},
)
.unwrap();
assert_eq!(seen_revs, vec![rev1, rev2]);
}
#[test]
fn test_get_file_revs_empty_range() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/file.txt").unwrap();
let rev1 = txn.commit().unwrap();
let mut count = 0usize;
repo.get_file_revs(
"/file.txt",
rev1,
rev1,
false,
|_path, _rev, _rev_props, _merged, _prop_diffs| {
count += 1;
Ok(())
},
)
.unwrap();
assert_eq!(count, 1, "file created in rev1 should appear once");
}
#[test]
fn test_repos_info_format() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let (format, version) = repo.info_format().unwrap();
assert!(format > 0, "repos format should be positive, got {format}");
let (major, minor, patch) = version;
assert!(
major >= 0 && minor >= 0 && patch >= 0,
"version fields should be non-negative: ({major}, {minor}, {patch})"
);
}
#[test]
fn test_node_location_segments_simple() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "create file")
.unwrap();
txn.root().unwrap().make_file("/hello.txt").unwrap();
let rev1 = repo.commit_txn(txn).unwrap().0;
let mut segments: Vec<(Option<String>, crate::Revnum, crate::Revnum)> = Vec::new();
repo.node_location_segments("/hello.txt", rev1, rev1, rev1, |path, start, end| {
segments.push((path.map(str::to_owned), start, end));
Ok(())
})
.unwrap();
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].0.as_deref(), Some("hello.txt"));
assert_eq!(segments[0].1, rev1);
assert_eq!(segments[0].2, rev1);
}
#[test]
fn test_list_root_directory() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
{
let mut root = txn.root().unwrap();
root.make_dir("/trunk").unwrap();
root.make_file("/trunk/foo.txt").unwrap();
root.make_file("/trunk/bar.txt").unwrap();
}
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let mut entries: Vec<String> = Vec::new();
list(
&root,
"/trunk",
&[],
crate::Depth::Immediates,
false,
|path, _entry| {
entries.push(path.to_owned());
Ok(())
},
)
.unwrap();
entries.sort();
assert_eq!(entries, vec!["/trunk", "/trunk/bar.txt", "/trunk/foo.txt"]);
}
#[test]
fn test_list_with_pattern() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
{
let mut root = txn.root().unwrap();
root.make_dir("/src").unwrap();
root.make_file("/src/main.rs").unwrap();
root.make_file("/src/lib.rs").unwrap();
root.make_file("/src/README.md").unwrap();
}
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let mut entries: Vec<String> = Vec::new();
list(
&root,
"/src",
&["*.rs"],
crate::Depth::Immediates,
false,
|path, _entry| {
entries.push(path.to_owned());
Ok(())
},
)
.unwrap();
entries.sort();
assert_eq!(entries, vec!["/src/lib.rs", "/src/main.rs"]);
}
#[test]
fn test_list_dirent_fields() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
{
let mut root = txn.root().unwrap();
root.make_dir("/docs").unwrap();
root.make_file("/docs/guide.txt").unwrap();
}
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let mut entries: Vec<(String, RepoDirEntry)> = Vec::new();
list(
&root,
"/docs",
&[],
crate::Depth::Immediates,
false,
|path, entry| {
entries.push((path.to_owned(), entry));
Ok(())
},
)
.unwrap();
let file_entry = entries
.iter()
.find(|(p, _)| p == "/docs/guide.txt")
.expect("should have found /docs/guide.txt");
assert_eq!(file_entry.1.kind(), crate::NodeKind::File);
assert_eq!(file_entry.1.created_rev(), rev1);
let dir_entry = entries
.iter()
.find(|(p, _)| p == "/docs")
.expect("should have found /docs directory entry");
assert_eq!(dir_entry.1.kind(), crate::NodeKind::Dir);
}
#[test]
#[cfg(feature = "delta")]
fn test_replay_runs_without_error() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
{
let mut root = txn.root().unwrap();
root.make_file("/hello.txt").unwrap();
}
let rev1 = txn.commit().unwrap();
let root = fs.revision_root(rev1).unwrap();
let pool = apr::Pool::new();
let editor = crate::delta::default_editor(pool);
replay(&root, "", crate::Revnum(-1), false, &editor).unwrap();
}
#[test]
fn test_fs_get_mergeinfo_empty_when_not_set() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::Revnum(0), 0).unwrap();
txn.root().unwrap().make_file("/hello.txt").unwrap();
let rev1 = txn.commit().unwrap();
let mut received: Vec<String> = Vec::new();
repo.fs_get_mergeinfo(
&["/hello.txt"],
rev1,
crate::mergeinfo::MergeinfoInheritance::Explicit,
false,
|path, _mi| {
received.push(path.to_owned());
Ok(())
},
)
.unwrap();
assert_eq!(received, Vec::<String>::new());
}
#[test]
fn test_fs_get_mergeinfo_returns_value_when_set() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let repo = Repos::create(&repo_path).unwrap();
let fs = repo.fs().unwrap();
let mut txn = fs.begin_txn(crate::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 mut received: Vec<(String, String)> = Vec::new();
repo.fs_get_mergeinfo(
&["/trunk"],
rev2,
crate::mergeinfo::MergeinfoInheritance::Explicit,
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_repos_fs_lock_and_unlock() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let mut repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add file")
.unwrap();
txn.root().unwrap().make_file("/file.txt").unwrap();
repo.commit_txn(txn).unwrap();
let mut fs_for_access = repo.fs().unwrap();
fs_for_access.set_access("testuser").unwrap();
let lock = repo
.fs_lock(
"/file.txt",
None,
Some("test lock"),
false,
None,
crate::Revnum::invalid(),
false,
)
.expect("fs_lock should succeed");
let token = lock.token().to_string();
assert_eq!(lock.path(), "/file.txt");
assert_eq!(lock.comment(), "test lock");
repo.fs_unlock("/file.txt", &token, false)
.expect("fs_unlock should succeed");
let fs = repo.fs().unwrap();
let retrieved = fs.get_lock("/file.txt").unwrap();
assert!(retrieved.is_none(), "Lock should have been removed");
}
#[test]
fn test_repos_fs_lock_many_and_unlock_many() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let mut repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add files")
.unwrap();
{
let mut root = txn.root().unwrap();
root.make_file("/a.txt").unwrap();
root.make_file("/b.txt").unwrap();
}
repo.commit_txn(txn).unwrap();
let mut fs = repo.fs().unwrap();
fs.set_access("testuser").unwrap();
let targets = [
("/a.txt", None, crate::Revnum::invalid()),
("/b.txt", None, crate::Revnum::invalid()),
];
let mut locked_paths = Vec::new();
repo.fs_lock_many(&targets, None, false, None, false, |path, err| {
assert!(err.is_none(), "Lock error: {:?}", err);
locked_paths.push(path.to_string());
})
.expect("fs_lock_many should succeed");
assert_eq!(locked_paths.len(), 2);
let fs = repo.fs().unwrap();
let token_a = fs.get_lock("/a.txt").unwrap().unwrap().token().to_string();
let token_b = fs.get_lock("/b.txt").unwrap().unwrap().token().to_string();
let unlock_targets = [("/a.txt", token_a.as_str()), ("/b.txt", token_b.as_str())];
let mut unlocked_paths = Vec::new();
repo.fs_unlock_many(&unlock_targets, false, |path, err| {
assert!(err.is_none(), "Unlock error: {:?}", err);
unlocked_paths.push(path.to_string());
})
.expect("fs_unlock_many should succeed");
assert_eq!(unlocked_paths.len(), 2);
}
#[test]
fn test_repos_fs_get_locks() {
let temp_dir = tempfile::tempdir().unwrap();
let repo_path = temp_dir.path().join("test_repo");
let mut repo = Repos::create(&repo_path).unwrap();
let mut txn = repo
.begin_txn_for_commit(crate::Revnum(0), "author", "add file")
.unwrap();
txn.root().unwrap().make_file("/locked.txt").unwrap();
repo.commit_txn(txn).unwrap();
let mut fs_for_access = repo.fs().unwrap();
fs_for_access.set_access("testuser").unwrap();
repo.fs_lock(
"/locked.txt",
None,
None,
false,
None,
crate::Revnum::invalid(),
false,
)
.expect("Lock should succeed");
let locks = repo
.fs_get_locks("/", crate::Depth::Infinity, None)
.expect("fs_get_locks should succeed");
assert_eq!(locks.len(), 1, "Should have exactly one lock");
assert_eq!(locks[0].path(), "/locked.txt");
}
}