use crate::dirent::AsCanonicalDirent;
use crate::io::Dirent;
use crate::uri::AsCanonicalUri;
use crate::{
svn_result, with_tmp_pool, Depth, Error, LogEntry, OwnedLogEntry, Revision, RevisionRange,
Revnum, Version,
};
use apr::Pool;
use std::collections::HashMap;
use std::ops::ControlFlow;
use subversion_sys::svn_error_t;
use subversion_sys::{
svn_client_add5, svn_client_checkout3, svn_client_cleanup2, svn_client_commit6,
svn_client_conflict_get, svn_client_create_context2, svn_client_ctx_t, svn_client_delete4,
svn_client_export5, svn_client_import5, svn_client_log5, svn_client_mkdir4,
svn_client_proplist4, svn_client_status6, svn_client_switch3, svn_client_update4,
svn_client_vacuum, svn_client_version,
};
fn validate_absolute_path_or_url(path_or_url: &str) -> Result<(), Error<'static>> {
if crate::path::is_url(path_or_url) {
return Ok(());
}
let path_std = std::path::Path::new(path_or_url);
if !path_std.is_absolute() {
return Err(Error::from_message(&format!(
"Path must be absolute, got relative path: '{}'",
path_or_url
)));
}
Ok(())
}
pub fn version() -> Version {
unsafe { Version(svn_client_version()) }
}
extern "C" fn wrap_filter_callback(
baton: *mut std::ffi::c_void,
filtered: *mut subversion_sys::svn_boolean_t,
local_abspath: *const i8,
dirent: *const subversion_sys::svn_io_dirent2_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut svn_error_t {
unsafe {
let callback = &mut *(baton
as *mut &mut dyn FnMut(&std::path::Path, &Dirent) -> Result<bool, Error<'static>>);
let local_abspath: &std::path::Path = std::ffi::CStr::from_ptr(local_abspath)
.to_str()
.unwrap()
.as_ref();
let ret = callback(local_abspath, &Dirent::from(dirent));
if let Ok(ret) = ret {
*filtered = ret as subversion_sys::svn_boolean_t;
std::ptr::null_mut()
} else {
ret.unwrap_err().as_mut_ptr()
}
}
}
extern "C" fn wrap_status_func(
baton: *mut std::ffi::c_void,
path: *const i8,
status: *const subversion_sys::svn_client_status_t,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let callback =
&mut *(baton as *mut &mut dyn FnMut(&str, &Status) -> Result<(), Error<'static>>);
let local_path = subversion_sys::svn_dirent_local_style(path, pool);
let path_str = std::ffi::CStr::from_ptr(local_path).to_str().unwrap();
let pool_handle = apr::PoolHandle::from_borrowed_raw(pool);
let status_wrapper = Status {
ptr: status,
_pool: pool_handle,
};
let ret = callback(path_str, &status_wrapper);
if let Err(err) = ret {
err.into_raw()
} else {
std::ptr::null_mut()
}
}
}
extern "C" fn cancel_trampoline(baton: *mut std::ffi::c_void) -> *mut subversion_sys::svn_error_t {
if baton.is_null() {
return std::ptr::null_mut();
}
let handler = unsafe { &mut *(baton as *mut Box<dyn FnMut() -> bool + Send>) };
if handler() {
let err = Error::new(
apr::Status::from(200015_i32),
None,
"Operation cancelled by user",
);
unsafe { err.into_raw() }
} else {
std::ptr::null_mut()
}
}
extern "C" fn log_msg_trampoline(
log_msg: *mut *const std::os::raw::c_char,
tmp_file: *mut *const std::os::raw::c_char,
commit_items: *const apr_sys::apr_array_header_t,
baton: *mut std::ffi::c_void,
pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() {
unsafe {
*log_msg = std::ptr::null();
*tmp_file = std::ptr::null();
}
return std::ptr::null_mut();
}
unsafe {
let callback =
&mut *(baton as *mut &mut dyn FnMut(&[CommitItem]) -> Result<String, Error<'static>>);
let items_vec = if commit_items.is_null() {
Vec::new()
} else {
let array = &*commit_items;
let count = array.nelts as usize;
let elts = array.elts as *const *const subversion_sys::svn_client_commit_item3_t;
(0..count)
.map(|i| CommitItem::from_raw(*elts.add(i)))
.collect::<Vec<_>>()
};
match callback(&items_vec) {
Ok(msg) => {
let pool_handle = apr::PoolHandle::from_borrowed_raw(pool);
let duplicated = apr::strings::pstrdup(&msg, &pool_handle).unwrap().as_ptr();
*log_msg = duplicated;
*tmp_file = std::ptr::null();
std::ptr::null_mut()
}
Err(err) => {
*log_msg = std::ptr::null();
*tmp_file = std::ptr::null();
err.into_raw()
}
}
}
}
extern "C" fn wrap_proplist_receiver2(
baton: *mut std::ffi::c_void,
path: *const i8,
props: *mut apr::hash::apr_hash_t,
inherited_props: *mut apr::tables::apr_array_header_t,
scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let _scratch_pool = apr::PoolHandle::from_borrowed_raw(scratch_pool);
let callback = baton
as *mut &mut dyn FnMut(
&str,
&HashMap<String, Vec<u8>>,
Option<&[crate::InheritedItem]>,
) -> Result<(), Error<'static>>;
let callback = &mut *(callback);
let path: &str = std::ffi::CStr::from_ptr(path).to_str().unwrap();
let prop_hash = crate::props::PropHash::from_ptr(props);
let props = prop_hash.to_hashmap();
let inherited_props = if inherited_props.is_null() {
None
} else {
let inherited_props = apr::tables::TypedArray::<
*mut subversion_sys::svn_prop_inherited_item_t,
>::from_ptr(inherited_props);
Some(
inherited_props
.iter()
.map(|x| crate::InheritedItem::from_raw(x))
.collect::<Vec<_>>(),
)
};
let ret = callback(path, &props, inherited_props.as_deref());
if let Err(err) = ret {
err.into_raw()
} else {
std::ptr::null_mut()
}
}
}
extern "C" fn wrap_changelist_receiver(
baton: *mut std::ffi::c_void,
path: *const std::os::raw::c_char,
changelist: *const std::os::raw::c_char,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
if changelist.is_null() {
return std::ptr::null_mut();
}
let callback = baton as *mut &mut dyn FnMut(&str, &str) -> Result<(), Error<'static>>;
let callback = &mut *callback;
let path_str = std::ffi::CStr::from_ptr(path).to_str().unwrap();
let changelist_str = std::ffi::CStr::from_ptr(changelist).to_str().unwrap();
match callback(path_str, changelist_str) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
}
pub struct Info(*const subversion_sys::svn_client_info2_t);
pub struct BlameInfo {
pub line_no: i64,
pub revision: Revnum,
pub revprops: HashMap<String, Vec<u8>>,
pub merged_revision: Option<Revnum>,
pub merged_revprops: HashMap<String, Vec<u8>>,
pub merged_path: Option<String>,
pub line: String,
pub local_change: bool,
}
impl Info {
pub fn url(&self) -> &str {
unsafe {
let url = (*self.0).URL;
std::ffi::CStr::from_ptr(url).to_str().unwrap()
}
}
pub fn revision(&self) -> Revnum {
unsafe { Revnum::from_raw((*self.0).rev).unwrap() }
}
pub fn kind(&self) -> subversion_sys::svn_node_kind_t {
unsafe { (*self.0).kind }
}
pub fn repos_root_url(&self) -> &str {
unsafe {
let url = (*self.0).repos_root_URL;
std::ffi::CStr::from_ptr(url).to_str().unwrap()
}
}
pub fn repos_uuid(&self) -> &str {
unsafe {
let uuid = (*self.0).repos_UUID;
std::ffi::CStr::from_ptr(uuid).to_str().unwrap()
}
}
pub fn last_changed_rev(&self) -> Option<Revnum> {
Revnum::from_raw(unsafe { (*self.0).last_changed_rev })
}
pub fn last_changed_date(&self) -> apr::time::Time {
unsafe { (*self.0).last_changed_date.into() }
}
pub fn last_changed_author(&self) -> Option<&str> {
unsafe {
let author = (*self.0).last_changed_author;
if author.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(author).to_str().unwrap())
}
}
}
pub fn size(&self) -> i64 {
unsafe { (*self.0).size }
}
pub fn wc_info(&self) -> Option<WcInfo> {
unsafe {
let wc = (*self.0).wc_info;
if wc.is_null() {
None
} else {
Some(WcInfo(wc))
}
}
}
}
pub struct WcInfo(*const subversion_sys::svn_wc_info_t);
pub struct OwnedWcInfo {
info: WcInfo,
_pool: apr::Pool<'static>,
}
impl WcInfo {
pub fn dup(&self) -> OwnedWcInfo {
let pool = apr::Pool::new();
let ptr = unsafe { subversion_sys::svn_wc_info_dup(self.0, pool.as_mut_ptr()) };
OwnedWcInfo {
info: WcInfo(ptr),
_pool: pool,
}
}
pub fn schedule(&self) -> subversion_sys::svn_wc_schedule_t {
unsafe { (*self.0).schedule }
}
pub fn copyfrom_url(&self) -> Option<&str> {
unsafe {
let url = (*self.0).copyfrom_url;
if url.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(url).to_str().unwrap())
}
}
}
pub fn copyfrom_rev(&self) -> Option<Revnum> {
Revnum::from_raw(unsafe { (*self.0).copyfrom_rev })
}
pub fn changelist(&self) -> Option<&str> {
unsafe {
let cl = (*self.0).changelist;
if cl.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(cl).to_str().unwrap())
}
}
}
pub fn depth(&self) -> subversion_sys::svn_depth_t {
unsafe { (*self.0).depth }
}
pub fn recorded_size(&self) -> i64 {
unsafe { (*self.0).recorded_size }
}
pub fn recorded_time(&self) -> apr::time::Time {
unsafe { (*self.0).recorded_time.into() }
}
pub fn wcroot_abspath(&self) -> Option<&str> {
unsafe {
let p = (*self.0).wcroot_abspath;
if p.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
}
}
}
pub fn moved_from_abspath(&self) -> Option<&str> {
unsafe {
let p = (*self.0).moved_from_abspath;
if p.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
}
}
}
pub fn moved_to_abspath(&self) -> Option<&str> {
unsafe {
let p = (*self.0).moved_to_abspath;
if p.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
}
}
}
}
impl OwnedWcInfo {
pub fn from_wc_info(info: &WcInfo) -> Self {
info.dup()
}
}
unsafe impl Send for OwnedWcInfo {}
impl std::ops::Deref for OwnedWcInfo {
type Target = WcInfo;
fn deref(&self) -> &Self::Target {
&self.info
}
}
extern "C" fn wrap_info_receiver2(
baton: *mut std::ffi::c_void,
abspath_or_url: *const i8,
info: *const subversion_sys::svn_client_info2_t,
_scatch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let callback = &mut *(baton
as *mut &mut dyn FnMut(&std::path::Path, &Info) -> Result<(), Error<'static>>);
let abspath_or_url: &std::path::Path = std::ffi::CStr::from_ptr(abspath_or_url)
.to_str()
.unwrap()
.as_ref();
let ret = callback(abspath_or_url, &Info(info));
if let Err(err) = ret {
err.into_raw()
} else {
std::ptr::null_mut()
}
}
}
unsafe extern "C" fn blame_receiver_wrapper(
baton: *mut std::ffi::c_void,
line_no: apr_sys::apr_int64_t,
revision: subversion_sys::svn_revnum_t,
rev_props: *mut apr_sys::apr_hash_t,
merged_revision: subversion_sys::svn_revnum_t,
merged_rev_props: *mut apr_sys::apr_hash_t,
merged_path: *const i8,
line: *const subversion_sys::svn_string_t,
local_change: subversion_sys::svn_boolean_t,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let callback = &mut *(baton as *mut &mut dyn FnMut(BlameInfo) -> Result<(), Error<'static>>);
let revprops = if !rev_props.is_null() {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(rev_props) };
prop_hash.to_hashmap()
} else {
HashMap::new()
};
let merged_revprops = if !merged_rev_props.is_null() {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(merged_rev_props) };
prop_hash.to_hashmap()
} else {
HashMap::new()
};
let line_str = if !line.is_null() {
let line_ref = &*line;
let str_data = line_ref.data;
let str_len = line_ref.len;
let bytes = std::slice::from_raw_parts(str_data as *const u8, str_len);
String::from_utf8_lossy(bytes).into_owned()
} else {
String::new()
};
let merged_path_str = if !merged_path.is_null() {
Some(
std::ffi::CStr::from_ptr(merged_path)
.to_string_lossy()
.into_owned(),
)
} else {
None
};
let info = BlameInfo {
line_no,
revision: Revnum::from_raw(revision).unwrap_or(Revnum::from(0u64)),
revprops,
merged_revision: Revnum::from_raw(merged_revision),
merged_revprops,
merged_path: merged_path_str,
line: line_str,
local_change: local_change != 0,
};
match callback(info) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => e.as_mut_ptr(),
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CatOptions {
pub revision: Revision,
pub peg_revision: Revision,
pub expand_keywords: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CleanupOptions {
pub break_locks: bool,
pub fix_recorded_timestamps: bool,
pub clear_dav_cache: bool,
pub vacuum_pristines: bool,
pub include_externals: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ProplistOptions<'a> {
pub peg_revision: Revision,
pub revision: Revision,
pub depth: Depth,
pub changelists: Option<&'a [&'a str]>,
pub get_target_inherited_props: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ExportOptions {
pub peg_revision: Revision,
pub revision: Revision,
pub overwrite: bool,
pub ignore_externals: bool,
pub ignore_keywords: bool,
pub depth: Depth,
pub native_eol: crate::NativeEOL,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct VacuumOptions {
pub remove_unversioned_items: bool,
pub remove_ignored_items: bool,
pub fix_recorded_timestamps: bool,
pub vacuum_pristines: bool,
pub include_externals: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct CheckoutOptions {
pub peg_revision: Revision,
pub revision: Revision,
pub depth: Depth,
pub ignore_externals: bool,
pub allow_unver_obstructions: bool,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct UpdateOptions {
pub depth: Depth,
pub depth_is_sticky: bool,
pub ignore_externals: bool,
pub allow_unver_obstructions: bool,
pub adds_as_modifications: bool,
pub make_parents: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct SwitchOptions {
pub peg_revision: Revision,
pub revision: Revision,
pub depth: Depth,
pub depth_is_sticky: bool,
pub ignore_externals: bool,
pub allow_unver_obstructions: bool,
pub ignore_ancestry: bool,
}
impl Default for SwitchOptions {
fn default() -> Self {
Self {
peg_revision: Revision::Unspecified,
revision: Revision::Head,
depth: Depth::Infinity,
depth_is_sticky: false,
ignore_externals: false,
allow_unver_obstructions: false,
ignore_ancestry: false,
}
}
}
impl SwitchOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
self.peg_revision = peg_revision;
self
}
pub fn with_revision(mut self, revision: Revision) -> Self {
self.revision = revision;
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_depth_is_sticky(mut self, depth_is_sticky: bool) -> Self {
self.depth_is_sticky = depth_is_sticky;
self
}
pub fn with_ignore_externals(mut self, ignore_externals: bool) -> Self {
self.ignore_externals = ignore_externals;
self
}
pub fn with_allow_unver_obstructions(mut self, allow_unver_obstructions: bool) -> Self {
self.allow_unver_obstructions = allow_unver_obstructions;
self
}
pub fn with_ignore_ancestry(mut self, ignore_ancestry: bool) -> Self {
self.ignore_ancestry = ignore_ancestry;
self
}
}
#[derive(Debug, Clone, Copy)]
pub struct AddOptions {
pub depth: Depth,
pub force: bool,
pub no_ignore: bool,
pub no_autoprops: bool,
pub add_parents: bool,
}
impl Default for AddOptions {
fn default() -> Self {
Self {
depth: Depth::Infinity,
force: false,
no_ignore: false,
no_autoprops: false,
add_parents: false,
}
}
}
impl AddOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_force(mut self, force: bool) -> Self {
self.force = force;
self
}
pub fn with_no_ignore(mut self, no_ignore: bool) -> Self {
self.no_ignore = no_ignore;
self
}
pub fn with_no_autoprops(mut self, no_autoprops: bool) -> Self {
self.no_autoprops = no_autoprops;
self
}
pub fn with_add_parents(mut self, add_parents: bool) -> Self {
self.add_parents = add_parents;
self
}
}
#[derive(Default)]
pub struct DeleteOptions<'a> {
pub force: bool,
pub keep_local: bool,
pub commit_callback:
Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}
impl<'a> DeleteOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_force(mut self, force: bool) -> Self {
self.force = force;
self
}
pub fn with_keep_local(mut self, keep_local: bool) -> Self {
self.keep_local = keep_local;
self
}
pub fn with_commit_callback(
mut self,
callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Self {
self.commit_callback = Some(callback);
self
}
}
#[derive(Default)]
pub struct CopyOptions<'a> {
pub copy_as_child: bool,
pub make_parents: bool,
pub ignore_externals: bool,
pub metadata_only: bool,
pub pin_externals: bool,
pub externals_to_pin: Option<std::collections::HashMap<String, String>>,
pub revprop_table: Option<std::collections::HashMap<String, String>>,
pub commit_callback:
Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}
impl<'a> CopyOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_copy_as_child(mut self, copy_as_child: bool) -> Self {
self.copy_as_child = copy_as_child;
self
}
pub fn with_make_parents(mut self, make_parents: bool) -> Self {
self.make_parents = make_parents;
self
}
pub fn with_ignore_externals(mut self, ignore_externals: bool) -> Self {
self.ignore_externals = ignore_externals;
self
}
pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
self.metadata_only = metadata_only;
self
}
pub fn with_pin_externals(mut self, pin_externals: bool) -> Self {
self.pin_externals = pin_externals;
self
}
pub fn with_externals_to_pin(
mut self,
externals: std::collections::HashMap<String, String>,
) -> Self {
self.externals_to_pin = Some(externals);
self
}
pub fn with_revprop_table(
mut self,
revprops: std::collections::HashMap<String, String>,
) -> Self {
self.revprop_table = Some(revprops);
self
}
pub fn with_commit_callback(
mut self,
callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Self {
self.commit_callback = Some(callback);
self
}
}
pub struct MoveOptions<'a> {
pub move_as_child: bool,
pub make_parents: bool,
pub allow_mixed_revisions: bool,
pub metadata_only: bool,
pub revprop_table: Option<std::collections::HashMap<String, Vec<u8>>>,
pub commit_callback:
Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}
impl<'a> Default for MoveOptions<'a> {
fn default() -> Self {
Self {
move_as_child: false,
make_parents: false,
allow_mixed_revisions: true,
metadata_only: false,
revprop_table: None,
commit_callback: None,
}
}
}
impl<'a> MoveOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_move_as_child(mut self, move_as_child: bool) -> Self {
self.move_as_child = move_as_child;
self
}
pub fn with_make_parents(mut self, make_parents: bool) -> Self {
self.make_parents = make_parents;
self
}
pub fn with_allow_mixed_revisions(mut self, allow_mixed_revisions: bool) -> Self {
self.allow_mixed_revisions = allow_mixed_revisions;
self
}
pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
self.metadata_only = metadata_only;
self
}
pub fn with_revprop_table(
mut self,
revprops: std::collections::HashMap<String, Vec<u8>>,
) -> Self {
self.revprop_table = Some(revprops);
self
}
pub fn with_commit_callback(
mut self,
callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Self {
self.commit_callback = Some(callback);
self
}
}
#[derive(Debug, Clone)]
pub struct MergeSourcesOptions {
pub ignore_mergeinfo: bool,
pub diff_ignore_ancestry: bool,
pub force_delete: bool,
pub record_only: bool,
pub dry_run: bool,
pub allow_mixed_rev: bool,
pub merge_options: Option<Vec<String>>,
}
impl Default for MergeSourcesOptions {
fn default() -> Self {
Self {
ignore_mergeinfo: false,
diff_ignore_ancestry: false,
force_delete: false,
record_only: false,
dry_run: false,
allow_mixed_rev: true,
merge_options: None,
}
}
}
impl MergeSourcesOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_ignore_mergeinfo(mut self, ignore: bool) -> Self {
self.ignore_mergeinfo = ignore;
self
}
pub fn with_diff_ignore_ancestry(mut self, ignore: bool) -> Self {
self.diff_ignore_ancestry = ignore;
self
}
pub fn with_force_delete(mut self, force: bool) -> Self {
self.force_delete = force;
self
}
pub fn with_record_only(mut self, record_only: bool) -> Self {
self.record_only = record_only;
self
}
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub fn with_allow_mixed_rev(mut self, allow: bool) -> Self {
self.allow_mixed_rev = allow;
self
}
pub fn with_merge_options(mut self, options: Vec<String>) -> Self {
self.merge_options = Some(options);
self
}
}
pub struct PatchOptions<'a> {
pub dry_run: bool,
pub strip_count: i32,
pub reverse: bool,
pub ignore_whitespace: bool,
pub remove_tempfiles: bool,
pub patch_func: Option<
&'a mut dyn FnMut(
&mut bool,
&str,
&std::path::Path,
&std::path::Path,
) -> Result<(), Error<'static>>,
>,
}
impl<'a> Default for PatchOptions<'a> {
fn default() -> Self {
Self {
dry_run: false,
strip_count: 0,
reverse: false,
ignore_whitespace: false,
remove_tempfiles: true,
patch_func: None,
}
}
}
impl<'a> PatchOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_dry_run(mut self, dry_run: bool) -> Self {
self.dry_run = dry_run;
self
}
pub fn with_strip_count(mut self, count: i32) -> Self {
self.strip_count = count;
self
}
pub fn with_reverse(mut self, reverse: bool) -> Self {
self.reverse = reverse;
self
}
pub fn with_ignore_whitespace(mut self, ignore: bool) -> Self {
self.ignore_whitespace = ignore;
self
}
pub fn with_remove_tempfiles(mut self, remove: bool) -> Self {
self.remove_tempfiles = remove;
self
}
pub fn with_patch_func(
mut self,
func: &'a mut dyn FnMut(
&mut bool,
&str,
&std::path::Path,
&std::path::Path,
) -> Result<(), Error<'static>>,
) -> Self {
self.patch_func = Some(func);
self
}
}
#[derive(Default)]
pub struct MkdirOptions<'a> {
pub make_parents: bool,
pub revprop_table: Option<std::collections::HashMap<String, Vec<u8>>>,
pub commit_callback:
Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}
impl<'a> MkdirOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_make_parents(mut self, make_parents: bool) -> Self {
self.make_parents = make_parents;
self
}
pub fn with_revprop_table(
mut self,
revprops: std::collections::HashMap<String, Vec<u8>>,
) -> Self {
self.revprop_table = Some(revprops);
self
}
pub fn with_commit_callback(
mut self,
callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Self {
self.commit_callback = Some(callback);
self
}
}
pub struct ImportOptions<'a> {
pub depth: Depth,
pub no_ignore: bool,
pub no_autoprops: bool,
pub ignore_unknown_node_types: bool,
pub revprop_table: Option<std::collections::HashMap<String, String>>,
pub filter_callback: Option<
&'a mut dyn FnMut(&mut bool, &std::path::Path, &Dirent) -> Result<(), Error<'static>>,
>,
pub commit_callback:
Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}
impl<'a> Default for ImportOptions<'a> {
fn default() -> Self {
Self {
depth: Depth::Infinity,
no_ignore: false,
no_autoprops: false,
ignore_unknown_node_types: false,
revprop_table: None,
filter_callback: None,
commit_callback: None,
}
}
}
impl<'a> ImportOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_no_ignore(mut self, no_ignore: bool) -> Self {
self.no_ignore = no_ignore;
self
}
pub fn with_no_autoprops(mut self, no_autoprops: bool) -> Self {
self.no_autoprops = no_autoprops;
self
}
pub fn with_ignore_unknown_node_types(mut self, ignore: bool) -> Self {
self.ignore_unknown_node_types = ignore;
self
}
pub fn with_revprop_table(
mut self,
revprops: std::collections::HashMap<String, String>,
) -> Self {
self.revprop_table = Some(revprops);
self
}
pub fn with_filter_callback(
mut self,
callback: &'a mut dyn FnMut(
&mut bool,
&std::path::Path,
&Dirent,
) -> Result<(), Error<'static>>,
) -> Self {
self.filter_callback = Some(callback);
self
}
pub fn with_commit_callback(
mut self,
callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Self {
self.commit_callback = Some(callback);
self
}
}
#[derive(Debug, Clone)]
pub struct RevertOptions {
pub depth: Depth,
pub changelists: Option<Vec<String>>,
pub clear_changelists: bool,
pub metadata_only: bool,
pub added_keep_local: bool,
}
impl Default for RevertOptions {
fn default() -> Self {
Self {
depth: Depth::Infinity,
changelists: None,
clear_changelists: false,
metadata_only: false,
added_keep_local: false,
}
}
}
impl RevertOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
pub fn with_clear_changelists(mut self, clear: bool) -> Self {
self.clear_changelists = clear;
self
}
pub fn with_metadata_only(mut self, metadata_only: bool) -> Self {
self.metadata_only = metadata_only;
self
}
pub fn with_added_keep_local(mut self, keep_local: bool) -> Self {
self.added_keep_local = keep_local;
self
}
}
#[derive(Debug, Clone)]
pub struct InheritedPropItem {
pub path_or_url: String,
pub properties: std::collections::HashMap<String, Vec<u8>>,
}
#[derive(Debug, Clone)]
pub struct PropGetOptions {
pub peg_revision: Revision,
pub revision: Revision,
pub depth: Depth,
pub changelists: Option<Vec<String>>,
}
impl Default for PropGetOptions {
fn default() -> Self {
Self {
peg_revision: Revision::Unspecified,
revision: Revision::Working,
depth: Depth::Empty,
changelists: None,
}
}
}
impl PropGetOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
self.peg_revision = peg_revision;
self
}
pub fn with_revision(mut self, revision: Revision) -> Self {
self.revision = revision;
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
#[derive(Debug, Clone)]
pub struct PropSetOptions {
pub depth: Depth,
pub skip_checks: bool,
pub base_revision_for_url: Revnum,
pub changelists: Option<Vec<String>>,
}
impl Default for PropSetOptions {
fn default() -> Self {
Self {
depth: Depth::Empty,
skip_checks: false,
base_revision_for_url: Revnum(-1),
changelists: None,
}
}
}
pub struct PropSetRemoteOptions<'a> {
pub skip_checks: bool,
pub base_revision_for_url: Revnum,
pub revprop_table: Option<std::collections::HashMap<String, String>>,
pub commit_callback:
Option<&'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>>,
}
impl<'a> Default for PropSetRemoteOptions<'a> {
fn default() -> Self {
Self {
skip_checks: false,
base_revision_for_url: Revnum(-1),
revprop_table: None,
commit_callback: None,
}
}
}
impl<'a> PropSetRemoteOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_skip_checks(mut self, skip_checks: bool) -> Self {
self.skip_checks = skip_checks;
self
}
pub fn with_base_revision_for_url(mut self, base_revision_for_url: Revnum) -> Self {
self.base_revision_for_url = base_revision_for_url;
self
}
pub fn with_revprop_table(
mut self,
revprop_table: std::collections::HashMap<String, String>,
) -> Self {
self.revprop_table = Some(revprop_table);
self
}
pub fn with_commit_callback(
mut self,
callback: &'a mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Self {
self.commit_callback = Some(callback);
self
}
}
impl PropSetOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_skip_checks(mut self, skip_checks: bool) -> Self {
self.skip_checks = skip_checks;
self
}
pub fn with_base_revision_for_url(mut self, base_revision: Revnum) -> Self {
self.base_revision_for_url = base_revision;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
#[derive(Debug, Clone)]
pub struct DiffOptions {
pub diff_options: Vec<String>,
pub depth: Depth,
pub ignore_ancestry: bool,
pub no_diff_added: bool,
pub no_diff_deleted: bool,
pub show_copies_as_adds: bool,
pub ignore_content_type: bool,
pub ignore_properties: bool,
pub properties_only: bool,
pub use_git_diff_format: bool,
pub pretty_print_mergeinfo: bool,
pub header_encoding: String,
pub changelists: Option<Vec<String>>,
}
impl Default for DiffOptions {
fn default() -> Self {
Self {
diff_options: Vec::new(),
depth: Depth::Infinity,
ignore_ancestry: false,
no_diff_added: false,
no_diff_deleted: false,
show_copies_as_adds: false,
ignore_content_type: false,
ignore_properties: false,
properties_only: false,
use_git_diff_format: false,
pretty_print_mergeinfo: false,
header_encoding: String::from("UTF-8"),
changelists: None,
}
}
}
impl DiffOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_diff_options(mut self, diff_options: Vec<String>) -> Self {
self.diff_options = diff_options;
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_ignore_ancestry(mut self, ignore_ancestry: bool) -> Self {
self.ignore_ancestry = ignore_ancestry;
self
}
pub fn with_no_diff_added(mut self, no_diff_added: bool) -> Self {
self.no_diff_added = no_diff_added;
self
}
pub fn with_no_diff_deleted(mut self, no_diff_deleted: bool) -> Self {
self.no_diff_deleted = no_diff_deleted;
self
}
pub fn with_show_copies_as_adds(mut self, show_copies_as_adds: bool) -> Self {
self.show_copies_as_adds = show_copies_as_adds;
self
}
pub fn with_ignore_content_type(mut self, ignore_content_type: bool) -> Self {
self.ignore_content_type = ignore_content_type;
self
}
pub fn with_ignore_properties(mut self, ignore_properties: bool) -> Self {
self.ignore_properties = ignore_properties;
self
}
pub fn with_properties_only(mut self, properties_only: bool) -> Self {
self.properties_only = properties_only;
self
}
pub fn with_use_git_diff_format(mut self, use_git_diff_format: bool) -> Self {
self.use_git_diff_format = use_git_diff_format;
self
}
pub fn with_header_encoding(mut self, header_encoding: String) -> Self {
self.header_encoding = header_encoding;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiffSummarizeKind {
Normal,
Added,
Modified,
Deleted,
}
impl From<subversion_sys::svn_client_diff_summarize_kind_t> for DiffSummarizeKind {
fn from(kind: subversion_sys::svn_client_diff_summarize_kind_t) -> Self {
match kind {
subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_normal => {
DiffSummarizeKind::Normal
}
subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_added => {
DiffSummarizeKind::Added
}
subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_modified => {
DiffSummarizeKind::Modified
}
subversion_sys::svn_client_diff_summarize_kind_t_svn_client_diff_summarize_kind_deleted => {
DiffSummarizeKind::Deleted
}
_ => unreachable!("unknown svn_client_diff_summarize_kind_t value: {}", kind),
}
}
}
#[derive(Debug, Clone)]
pub struct DiffSummary {
pub path: String,
pub kind: DiffSummarizeKind,
pub prop_changed: bool,
pub node_kind: subversion_sys::svn_node_kind_t,
}
impl DiffSummary {
pub(crate) unsafe fn from_raw(raw: *const subversion_sys::svn_client_diff_summarize_t) -> Self {
let path = std::ffi::CStr::from_ptr((*raw).path)
.to_string_lossy()
.into_owned();
Self {
path,
kind: (*raw).summarize_kind.into(),
prop_changed: (*raw).prop_changed != 0,
node_kind: (*raw).node_kind,
}
}
}
#[derive(Debug, Clone)]
pub struct DiffSummarizeOptions {
pub depth: Depth,
pub ignore_ancestry: bool,
pub changelists: Option<Vec<String>>,
}
impl Default for DiffSummarizeOptions {
fn default() -> Self {
Self {
depth: Depth::Infinity,
ignore_ancestry: false,
changelists: None,
}
}
}
impl DiffSummarizeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_ignore_ancestry(mut self, ignore_ancestry: bool) -> Self {
self.ignore_ancestry = ignore_ancestry;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
#[derive(Debug, Clone)]
pub struct LogOptions {
pub peg_revision: Revision,
pub limit: Option<i32>,
pub discover_changed_paths: bool,
pub strict_node_history: bool,
pub include_merged_revisions: bool,
pub revprops: Option<Vec<String>>,
}
impl Default for LogOptions {
fn default() -> Self {
Self {
peg_revision: Revision::Unspecified,
limit: None,
discover_changed_paths: false,
strict_node_history: false,
include_merged_revisions: false,
revprops: None,
}
}
}
impl LogOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
self.peg_revision = peg_revision;
self
}
pub fn with_limit(mut self, limit: i32) -> Self {
self.limit = Some(limit);
self
}
pub fn with_discover_changed_paths(mut self, discover: bool) -> Self {
self.discover_changed_paths = discover;
self
}
pub fn with_strict_node_history(mut self, strict: bool) -> Self {
self.strict_node_history = strict;
self
}
pub fn with_include_merged_revisions(mut self, include: bool) -> Self {
self.include_merged_revisions = include;
self
}
pub fn with_revprops(mut self, revprops: Option<Vec<String>>) -> Self {
self.revprops = revprops;
self
}
}
pub struct LogIterator<'a> {
rx: std::sync::mpsc::Receiver<Result<OwnedLogEntry, Error<'static>>>,
handle: Option<std::thread::JoinHandle<()>>,
_phantom: std::marker::PhantomData<&'a mut Context>,
}
impl Iterator for LogIterator<'_> {
type Item = Result<OwnedLogEntry, Error<'static>>;
fn next(&mut self) -> Option<Self::Item> {
self.rx.recv().ok()
}
}
impl Drop for LogIterator<'_> {
fn drop(&mut self) {
drop(std::mem::replace(
&mut self.rx,
std::sync::mpsc::sync_channel(0).1,
));
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
}
#[derive(Debug, Clone)]
pub struct MergeinfoLogOptions {
pub finding_merged: bool,
pub target_peg_revision: Revision,
pub source_peg_revision: Revision,
pub source_start_revision: Revision,
pub source_end_revision: Revision,
pub discover_changed_paths: bool,
pub depth: Depth,
pub revprops: Vec<String>,
}
impl Default for MergeinfoLogOptions {
fn default() -> Self {
Self {
finding_merged: true,
target_peg_revision: Revision::Head,
source_peg_revision: Revision::Head,
source_start_revision: Revision::Head,
source_end_revision: Revision::Number(Revnum(1)),
discover_changed_paths: false,
depth: Depth::Empty,
revprops: vec![],
}
}
}
impl MergeinfoLogOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_finding_merged(mut self, finding_merged: bool) -> Self {
self.finding_merged = finding_merged;
self
}
pub fn with_target_peg_revision(mut self, revision: Revision) -> Self {
self.target_peg_revision = revision;
self
}
pub fn with_source_peg_revision(mut self, revision: Revision) -> Self {
self.source_peg_revision = revision;
self
}
pub fn with_source_start_revision(mut self, revision: Revision) -> Self {
self.source_start_revision = revision;
self
}
pub fn with_source_end_revision(mut self, revision: Revision) -> Self {
self.source_end_revision = revision;
self
}
pub fn with_discover_changed_paths(mut self, discover: bool) -> Self {
self.discover_changed_paths = discover;
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_revprops(mut self, revprops: Vec<String>) -> Self {
self.revprops = revprops;
self
}
}
#[derive(Debug, Clone)]
pub struct BlameOptions {
pub peg_revision: Revision,
pub start_revision: Revision,
pub end_revision: Revision,
pub diff_options: Vec<String>,
pub ignore_mime_type: bool,
pub include_merged_revisions: bool,
}
impl Default for BlameOptions {
fn default() -> Self {
Self {
peg_revision: Revision::Unspecified,
start_revision: Revision::Number(Revnum(1)),
end_revision: Revision::Head,
diff_options: Vec::new(),
ignore_mime_type: false,
include_merged_revisions: false,
}
}
}
impl BlameOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
self.peg_revision = peg_revision;
self
}
pub fn with_start_revision(mut self, start_revision: Revision) -> Self {
self.start_revision = start_revision;
self
}
pub fn with_end_revision(mut self, end_revision: Revision) -> Self {
self.end_revision = end_revision;
self
}
pub fn with_diff_options(mut self, diff_options: Vec<String>) -> Self {
self.diff_options = diff_options;
self
}
pub fn with_ignore_mime_type(mut self, ignore: bool) -> Self {
self.ignore_mime_type = ignore;
self
}
pub fn with_include_merged_revisions(mut self, include: bool) -> Self {
self.include_merged_revisions = include;
self
}
}
#[derive(Debug, Clone)]
pub struct RevpropSetOptions {
pub revision: Revision,
pub original_propval: Option<Vec<u8>>,
pub force: bool,
}
impl Default for RevpropSetOptions {
fn default() -> Self {
Self {
revision: Revision::Head,
original_propval: None,
force: false,
}
}
}
impl RevpropSetOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_revision(mut self, revision: Revision) -> Self {
self.revision = revision;
self
}
pub fn with_original_propval(mut self, original_propval: Vec<u8>) -> Self {
self.original_propval = Some(original_propval);
self
}
pub fn with_force(mut self, force: bool) -> Self {
self.force = force;
self
}
}
#[derive(Debug, Clone)]
pub struct ListOptions {
pub peg_revision: Revision,
pub revision: Revision,
pub patterns: Option<Vec<String>>,
pub depth: Depth,
pub dirent_fields: u32,
pub fetch_locks: bool,
pub include_externals: bool,
}
impl Default for ListOptions {
fn default() -> Self {
Self {
peg_revision: Revision::Unspecified,
revision: Revision::Head,
patterns: None,
depth: Depth::Infinity,
dirent_fields: 0xFFFFFFFF, fetch_locks: false,
include_externals: false,
}
}
}
impl ListOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
self.peg_revision = peg_revision;
self
}
pub fn with_revision(mut self, revision: Revision) -> Self {
self.revision = revision;
self
}
pub fn with_patterns(mut self, patterns: Vec<String>) -> Self {
self.patterns = Some(patterns);
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_dirent_fields(mut self, dirent_fields: u32) -> Self {
self.dirent_fields = dirent_fields;
self
}
pub fn with_fetch_locks(mut self, fetch_locks: bool) -> Self {
self.fetch_locks = fetch_locks;
self
}
pub fn with_include_externals(mut self, include_externals: bool) -> Self {
self.include_externals = include_externals;
self
}
}
#[derive(Debug, Clone)]
pub struct InfoOptions {
pub peg_revision: Revision,
pub revision: Revision,
pub depth: Depth,
pub fetch_excluded: bool,
pub fetch_actual_only: bool,
pub include_externals: bool,
pub changelists: Option<Vec<String>>,
}
impl Default for InfoOptions {
fn default() -> Self {
Self {
peg_revision: Revision::Unspecified,
revision: Revision::Working,
depth: Depth::Empty,
fetch_excluded: false,
fetch_actual_only: false,
include_externals: false,
changelists: None,
}
}
}
impl InfoOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_peg_revision(mut self, peg_revision: Revision) -> Self {
self.peg_revision = peg_revision;
self
}
pub fn with_revision(mut self, revision: Revision) -> Self {
self.revision = revision;
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_fetch_excluded(mut self, fetch_excluded: bool) -> Self {
self.fetch_excluded = fetch_excluded;
self
}
pub fn with_fetch_actual_only(mut self, fetch_actual_only: bool) -> Self {
self.fetch_actual_only = fetch_actual_only;
self
}
pub fn with_include_externals(mut self, include_externals: bool) -> Self {
self.include_externals = include_externals;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
#[derive(Debug)]
pub struct CommitItem<'a> {
ptr: *const subversion_sys::svn_client_commit_item3_t,
_marker: std::marker::PhantomData<&'a ()>,
}
impl<'a> CommitItem<'a> {
fn from_raw(ptr: *const subversion_sys::svn_client_commit_item3_t) -> Self {
Self {
ptr,
_marker: std::marker::PhantomData,
}
}
pub fn path(&self) -> Option<&std::path::Path> {
unsafe {
if (*self.ptr).path.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).path)
.to_str()
.unwrap()
.as_ref(),
)
}
}
}
pub fn url(&self) -> Option<&str> {
unsafe {
if (*self.ptr).url.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr((*self.ptr).url).to_str().unwrap())
}
}
}
pub fn revision(&self) -> Revnum {
unsafe { Revnum((*self.ptr).revision) }
}
pub fn copyfrom_url(&self) -> Option<&str> {
unsafe {
if (*self.ptr).copyfrom_url.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).copyfrom_url)
.to_str()
.unwrap(),
)
}
}
}
pub fn copyfrom_rev(&self) -> Option<Revnum> {
unsafe {
if (*self.ptr).copyfrom_url.is_null() {
None
} else {
Some(Revnum((*self.ptr).copyfrom_rev))
}
}
}
}
#[derive(Debug, Clone)]
pub struct CommitOptions {
pub depth: Depth,
pub keep_locks: bool,
pub keep_changelists: bool,
pub commit_as_operations: bool,
pub include_file_externals: bool,
pub include_dir_externals: bool,
pub changelists: Option<Vec<String>>,
}
impl Default for CommitOptions {
fn default() -> Self {
Self {
depth: Depth::Infinity,
keep_locks: false,
keep_changelists: false,
commit_as_operations: true,
include_file_externals: false,
include_dir_externals: false,
changelists: None,
}
}
}
impl CommitOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_keep_locks(mut self, keep_locks: bool) -> Self {
self.keep_locks = keep_locks;
self
}
pub fn with_keep_changelists(mut self, keep_changelists: bool) -> Self {
self.keep_changelists = keep_changelists;
self
}
pub fn with_commit_as_operations(mut self, commit_as_operations: bool) -> Self {
self.commit_as_operations = commit_as_operations;
self
}
pub fn with_include_file_externals(mut self, include_file_externals: bool) -> Self {
self.include_file_externals = include_file_externals;
self
}
pub fn with_include_dir_externals(mut self, include_dir_externals: bool) -> Self {
self.include_dir_externals = include_dir_externals;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
#[derive(Debug, Clone)]
pub struct StatusOptions {
pub revision: Revision,
pub depth: Depth,
pub get_all: bool,
pub check_out_of_date: bool,
pub check_working_copy: bool,
pub no_ignore: bool,
pub ignore_externals: bool,
pub depth_as_sticky: bool,
pub changelists: Option<Vec<String>>,
}
impl Default for StatusOptions {
fn default() -> Self {
Self {
revision: Revision::Working,
depth: Depth::Infinity,
get_all: false,
check_out_of_date: false,
check_working_copy: true,
no_ignore: false,
ignore_externals: false,
depth_as_sticky: false,
changelists: None,
}
}
}
impl StatusOptions {
pub fn new() -> Self {
Self::default()
}
pub fn with_revision(mut self, revision: Revision) -> Self {
self.revision = revision;
self
}
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn with_get_all(mut self, get_all: bool) -> Self {
self.get_all = get_all;
self
}
pub fn with_check_out_of_date(mut self, check_out_of_date: bool) -> Self {
self.check_out_of_date = check_out_of_date;
self
}
pub fn with_check_working_copy(mut self, check_working_copy: bool) -> Self {
self.check_working_copy = check_working_copy;
self
}
pub fn with_no_ignore(mut self, no_ignore: bool) -> Self {
self.no_ignore = no_ignore;
self
}
pub fn with_ignore_externals(mut self, ignore_externals: bool) -> Self {
self.ignore_externals = ignore_externals;
self
}
pub fn with_depth_as_sticky(mut self, depth_as_sticky: bool) -> Self {
self.depth_as_sticky = depth_as_sticky;
self
}
pub fn with_changelists(mut self, changelists: Vec<String>) -> Self {
self.changelists = Some(changelists);
self
}
}
pub struct Context {
ptr: *mut svn_client_ctx_t,
pool: apr::Pool<'static>,
_phantom: std::marker::PhantomData<*mut ()>,
conflict_resolver: Option<Box<crate::conflict::ConflictResolverBaton>>,
cancel_handler: Option<Box<dyn FnMut() -> bool + Send>>,
auth_baton: Option<crate::auth::AuthBaton>,
config_hash: Option<crate::config::ConfigHash>,
}
unsafe impl Send for Context {}
impl Drop for Context {
fn drop(&mut self) {
if self.conflict_resolver.is_some() {
unsafe {
(*self.ptr).conflict_func2 = None;
(*self.ptr).conflict_baton2 = std::ptr::null_mut();
}
}
if self.cancel_handler.is_some() {
unsafe {
(*self.ptr).cancel_func = None;
(*self.ptr).cancel_baton = std::ptr::null_mut();
}
}
}
}
impl Context {
pub fn new() -> Result<Self, Error<'static>> {
Self::with_config_dir(None)
}
pub fn with_config_dir(config_dir: Option<&std::path::Path>) -> Result<Self, Error<'static>> {
crate::init::initialize()?;
let mut config_hash = crate::config::get_config_hash(config_dir)?;
let pool = apr::Pool::new();
let mut ctx = std::ptr::null_mut();
let ret = unsafe {
svn_client_create_context2(&mut ctx, config_hash.as_mut_ptr(), pool.as_mut_ptr())
};
Error::from_raw(ret)?;
Ok(Context {
ptr: ctx,
pool,
_phantom: std::marker::PhantomData,
conflict_resolver: None,
cancel_handler: None,
auth_baton: None,
config_hash: Some(config_hash),
})
}
pub(crate) unsafe fn as_mut_ptr(&mut self) -> *mut svn_client_ctx_t {
self.ptr
}
pub fn set_conflict_resolver(
&mut self,
resolver: impl crate::conflict::ConflictResolver + 'static,
) {
unsafe {
(*self.ptr).conflict_func2 = None;
(*self.ptr).conflict_baton2 = std::ptr::null_mut();
}
let baton = Box::new(crate::conflict::ConflictResolverBaton {
resolver: Box::new(resolver),
});
unsafe {
let baton_ptr = Box::into_raw(baton);
self.conflict_resolver = Some(Box::from_raw(baton_ptr));
(*self.ptr).conflict_func2 = Some(crate::conflict::conflict_resolver_callback);
(*self.ptr).conflict_baton2 = baton_ptr as *mut std::ffi::c_void;
}
}
pub fn clear_conflict_resolver(&mut self) {
unsafe {
(*self.ptr).conflict_func2 = None;
(*self.ptr).conflict_baton2 = std::ptr::null_mut();
}
self.conflict_resolver = None;
}
pub fn set_cancel_handler<F>(&mut self, handler: F)
where
F: FnMut() -> bool + Send + 'static,
{
unsafe {
(*self.ptr).cancel_func = None;
(*self.ptr).cancel_baton = std::ptr::null_mut();
}
self.cancel_handler = Some(Box::new(handler));
unsafe {
(*self.ptr).cancel_func = Some(cancel_trampoline);
(*self.ptr).cancel_baton = self
.cancel_handler
.as_mut()
.map(|h| h.as_mut() as *mut _ as *mut std::ffi::c_void)
.unwrap_or(std::ptr::null_mut());
}
}
pub fn clear_cancel_handler(&mut self) {
unsafe {
(*self.ptr).cancel_func = None;
(*self.ptr).cancel_baton = std::ptr::null_mut();
}
self.cancel_handler = None;
}
pub fn set_auth<'a, 'b>(&'a mut self, auth_baton: &'b mut crate::auth::AuthBaton)
where
'b: 'a,
{
unsafe {
(*self.ptr).auth_baton = auth_baton.as_mut_ptr();
}
}
pub unsafe fn set_auth_unchecked(&mut self, auth_baton: &mut crate::auth::AuthBaton) {
(*self.ptr).auth_baton = auth_baton.as_mut_ptr();
}
pub fn set_auth_owned(&mut self, mut auth_baton: crate::auth::AuthBaton) {
unsafe {
(*self.ptr).auth_baton = auth_baton.as_mut_ptr();
}
self.auth_baton = Some(auth_baton);
}
pub fn set_config(&mut self, mut config_hash: crate::config::ConfigHash) {
unsafe {
(*self.ptr).config = config_hash.as_mut_ptr();
}
self.config_hash = Some(config_hash);
}
pub fn pool(&self) -> &apr::Pool<'_> {
&self.pool
}
pub fn as_ptr(&self) -> *const svn_client_ctx_t {
self.ptr
}
pub fn checkout(
&mut self,
url: impl AsCanonicalUri,
path: impl AsCanonicalDirent,
options: &CheckoutOptions,
) -> Result<Revnum, Error<'static>> {
let peg_revision = options.peg_revision.into();
let revision = options.revision.into();
let url = url.as_canonical_uri()?;
let path = path.as_canonical_dirent()?;
with_tmp_pool(|pool| unsafe {
let mut revnum = 0;
let url_cstr = std::ffi::CString::new(url.as_str())?;
let path_cstr = std::ffi::CString::new(path.as_str())?;
let err = svn_client_checkout3(
&mut revnum,
url_cstr.as_ptr(),
path_cstr.as_ptr(),
&peg_revision,
&revision,
options.depth.into(),
options.ignore_externals.into(),
options.allow_unver_obstructions.into(),
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(Revnum::from_raw(revnum).unwrap())
})
}
pub fn update(
&mut self,
paths: &[&str],
revision: Revision,
options: &UpdateOptions,
) -> Result<Vec<Revnum>, Error<'static>> {
with_tmp_pool(|pool| unsafe {
let mut result_revs = std::ptr::null_mut();
let path_cstrings: Vec<std::ffi::CString> = paths
.iter()
.map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
.collect();
let mut ps = apr::tables::TypedArray::new(pool, paths.len() as i32);
for path in &path_cstrings {
ps.push(path.as_ptr() as *mut std::ffi::c_void);
}
let rev_c: crate::svn_opt_revision_t = revision.into();
let err = svn_client_update4(
&mut result_revs,
ps.as_ptr(),
&rev_c,
options.depth.into(),
options.depth_is_sticky.into(),
options.ignore_externals.into(),
options.allow_unver_obstructions.into(),
options.adds_as_modifications.into(),
options.make_parents.into(),
self.ptr,
pool.as_mut_ptr(),
);
let result_revs: apr::tables::TypedArray<Revnum> =
apr::tables::TypedArray::<Revnum>::from_ptr(result_revs);
Error::from_raw(err)?;
Ok(result_revs.iter().collect())
})
}
pub fn switch(
&mut self,
path: impl AsCanonicalDirent,
url: impl AsCanonicalUri,
options: &SwitchOptions,
) -> Result<Revnum, Error<'static>> {
let path = path.as_canonical_dirent()?;
let url = url.as_canonical_uri()?;
with_tmp_pool(|pool| unsafe {
let mut result_rev = 0;
let path_cstr = std::ffi::CString::new(path.as_str())?;
let url_cstr = std::ffi::CString::new(url.as_str())?;
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let rev_c: crate::svn_opt_revision_t = options.revision.into();
let err = svn_client_switch3(
&mut result_rev,
path_cstr.as_ptr(),
url_cstr.as_ptr(),
&peg_rev_c,
&rev_c,
options.depth.into(),
options.depth_is_sticky.into(),
options.ignore_externals.into(),
options.allow_unver_obstructions.into(),
options.ignore_ancestry.into(),
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(Revnum::from_raw(result_rev).unwrap())
})
}
pub fn add(
&mut self,
path: impl AsCanonicalDirent,
options: &AddOptions,
) -> Result<(), Error<'static>> {
let path = path.as_canonical_dirent()?;
with_tmp_pool(|pool| unsafe {
let path_cstr = std::ffi::CString::new(path.as_str())?;
let err = svn_client_add5(
path_cstr.as_ptr(),
options.depth.into(),
options.force.into(),
options.no_ignore.into(),
options.no_autoprops.into(),
options.add_parents.into(),
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
})
}
pub fn mkdir(
&mut self,
paths: &[&str],
options: &mut MkdirOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| unsafe {
let revprop_hash = options.revprop_table.as_ref().map(|revprops| {
let svn_strings: Vec<_> = revprops
.iter()
.map(|(k, v)| (k.as_str(), crate::string::BStr::from_bytes(v, pool)))
.collect();
apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
)
});
let revprop_ptr = revprop_hash
.as_ref()
.map_or(std::ptr::null_mut(), |h| h.as_ptr() as *mut _);
let path_cstrings: Vec<std::ffi::CString> = paths
.iter()
.map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
.collect();
let mut ps = apr::tables::TypedArray::new(pool, paths.len() as i32);
for path in &path_cstrings {
ps.push(path.as_ptr() as *mut std::ffi::c_void);
}
let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
{
(
Some(crate::wrap_commit_callback2 as _),
cb as *mut _ as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let err = svn_client_mkdir4(
ps.as_ptr(),
options.make_parents.into(),
revprop_ptr,
callback_func,
callback_baton,
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
})
}
pub fn delete(
&mut self,
paths: &[&str],
revprop_table: std::collections::HashMap<&str, &str>,
options: &mut DeleteOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| unsafe {
let svn_strings: Vec<_> = revprop_table
.iter()
.map(|(k, v)| (*k, crate::string::BStr::from_str(v, pool)))
.collect();
let rps = apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
);
let path_cstrings: Vec<std::ffi::CString> = paths
.iter()
.map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
.collect();
let mut ps = apr::tables::TypedArray::new(pool, paths.len() as i32);
for path in &path_cstrings {
ps.push(path.as_ptr() as *mut std::ffi::c_void);
}
let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
{
(
Some(crate::wrap_commit_callback2 as _),
cb as *mut _ as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let err = svn_client_delete4(
ps.as_ptr(),
options.force.into(),
options.keep_local.into(),
rps.as_ptr(),
callback_func,
callback_baton,
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
})
}
pub fn proplist(
&mut self,
target: &str,
options: &ProplistOptions,
receiver: &mut dyn FnMut(
&str,
&std::collections::HashMap<String, Vec<u8>>,
Option<&[crate::InheritedItem]>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let target_cstr = crate::dirent::canonicalize_path_or_url(target)?;
let changelists = options.changelists.map(|cl| {
cl.iter()
.map(|cl| std::ffi::CString::new(*cl).unwrap())
.collect::<Vec<_>>()
});
let changelists = changelists.as_ref().map(|cl| {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
for item in cl.iter() {
array.push(item.as_ptr());
}
array
});
unsafe {
let receiver = Box::into_raw(Box::new(receiver));
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let rev_c: crate::svn_opt_revision_t = options.revision.into();
let err = svn_client_proplist4(
target_cstr.as_ptr(),
&peg_rev_c,
&rev_c,
options.depth.into(),
changelists
.map(|cl| cl.as_ptr())
.unwrap_or(std::ptr::null()),
options.get_target_inherited_props.into(),
Some(wrap_proplist_receiver2),
receiver as *mut std::ffi::c_void,
self.ptr,
pool.as_mut_ptr(),
);
let _ = Box::from_raw(receiver);
Error::from_raw(err)?;
Ok(())
}
})
}
pub fn import(
&mut self,
path: impl AsCanonicalDirent,
url: &str,
options: &mut ImportOptions,
) -> Result<(), Error<'static>> {
let path = path.as_canonical_dirent()?;
with_tmp_pool(|pool| {
let revprop_hash = options.revprop_table.as_ref().map(|revprops| {
let svn_strings: Vec<_> = revprops
.iter()
.map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
.collect();
apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
)
});
let revprop_ptr = revprop_hash
.as_ref()
.map_or(std::ptr::null_mut(), |h| unsafe { h.as_ptr() as *mut _ });
unsafe {
let (filter_func, filter_baton) = if let Some(ref mut cb) = options.filter_callback
{
let boxed = Box::into_raw(Box::new(cb));
(
Some(wrap_filter_callback as _),
boxed as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let (commit_func, commit_baton) = if let Some(ref mut cb) = options.commit_callback
{
let boxed = Box::into_raw(Box::new(cb));
(
Some(crate::wrap_commit_callback2 as _),
boxed as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let path_cstr = std::ffi::CString::new(path.as_str())?;
let url_cstr = std::ffi::CString::new(url)?;
let err = svn_client_import5(
path_cstr.as_ptr(),
url_cstr.as_ptr(),
options.depth.into(),
options.no_ignore.into(),
options.no_autoprops.into(),
options.ignore_unknown_node_types.into(),
revprop_ptr,
filter_func,
filter_baton,
commit_func,
commit_baton,
self.ptr,
pool.as_mut_ptr(),
);
if !filter_baton.is_null() {
drop(Box::from_raw(
filter_baton
as *mut &mut dyn FnMut(
&mut bool,
&std::path::Path,
&Dirent,
)
-> Result<(), Error<'static>>,
));
}
if !commit_baton.is_null() {
drop(Box::from_raw(
commit_baton
as *mut &mut dyn FnMut(
&crate::CommitInfo,
)
-> Result<(), Error<'static>>,
));
}
Error::from_raw(err)?;
Ok(())
}
})
}
pub fn export(
&mut self,
from_path_or_url: &str,
to_path: impl AsCanonicalDirent,
options: &ExportOptions,
) -> Result<Option<Revnum>, Error<'static>> {
let native_eol: Option<&str> = options.native_eol.into();
let native_eol = native_eol.map(|s| std::ffi::CString::new(s).unwrap());
let mut revnum = 0;
let to_path = to_path.as_canonical_dirent()?;
with_tmp_pool(|tmp_pool| unsafe {
let path_cstr = crate::dirent::to_absolute_cstring(to_path)?;
let from_cstr = crate::dirent::canonicalize_path_or_url(from_path_or_url)?;
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let rev_c: crate::svn_opt_revision_t = options.revision.into();
let err = svn_client_export5(
&mut revnum,
from_cstr.as_ptr(),
path_cstr.as_ptr(),
&peg_rev_c,
&rev_c,
options.overwrite as i32,
options.ignore_externals as i32,
options.ignore_keywords as i32,
options.depth.into(),
native_eol.map(|s| s.as_ptr()).unwrap_or(std::ptr::null()),
self.ptr,
tmp_pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(Revnum::from_raw(revnum))
})
}
pub fn commit(
&mut self,
targets: &[&str],
options: &CommitOptions,
mut revprop_table: std::collections::HashMap<&str, &str>,
log_msg_func: Option<&mut dyn FnMut(&[CommitItem]) -> Result<String, Error<'static>>>,
commit_callback: &mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let log_message = revprop_table.remove("svn:log").map(|s| s.to_string());
let svn_strings: Vec<_> = revprop_table
.iter()
.map(|(k, v)| (*k, crate::string::BStr::from_str(v, pool)))
.collect();
let rps = apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
);
unsafe {
let mut default_log_func;
let log_msg_callback: &mut dyn FnMut(
&[CommitItem],
)
-> Result<String, Error<'static>> = if let Some(func) = log_msg_func {
func
} else {
default_log_func =
|_items: &[CommitItem]| Ok(log_message.clone().unwrap_or_default());
&mut default_log_func
};
let log_msg_baton = Box::into_raw(Box::new(log_msg_callback));
let old_log_msg_func = (*self.ptr).log_msg_func3;
let old_log_msg_baton = (*self.ptr).log_msg_baton3;
(*self.ptr).log_msg_func3 = Some(log_msg_trampoline);
(*self.ptr).log_msg_baton3 = log_msg_baton as *mut std::ffi::c_void;
let target_cstrings: Vec<std::ffi::CString> = targets
.iter()
.map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
.collect();
let mut ps = apr::tables::TypedArray::new(pool, targets.len() as i32);
for target in &target_cstrings {
ps.push(target.as_ptr() as *mut std::ffi::c_void);
}
let changelist_cstrings: Vec<std::ffi::CString> =
if let Some(changelists) = &options.changelists {
changelists
.iter()
.map(|c| std::ffi::CString::new(c.as_str()).unwrap())
.collect()
} else {
Vec::new()
};
let mut cl = apr::tables::TypedArray::new(pool, changelist_cstrings.len() as i32);
for changelist in &changelist_cstrings {
cl.push(changelist.as_ptr() as *mut std::ffi::c_void);
}
let commit_callback_baton = Box::into_raw(Box::new(commit_callback));
let err = svn_client_commit6(
ps.as_ptr(),
options.depth.into(),
options.keep_locks.into(),
options.keep_changelists.into(),
options.commit_as_operations.into(),
options.include_file_externals.into(),
options.include_dir_externals.into(),
cl.as_ptr(),
rps.as_ptr(),
Some(crate::wrap_commit_callback2),
commit_callback_baton as *mut std::ffi::c_void,
self.ptr,
pool.as_mut_ptr(),
);
(*self.ptr).log_msg_func3 = old_log_msg_func;
(*self.ptr).log_msg_baton3 = old_log_msg_baton;
drop(Box::from_raw(log_msg_baton));
drop(Box::from_raw(commit_callback_baton));
Error::from_raw(err)?;
Ok(())
}
})
}
pub fn status(
&mut self,
path: &str,
options: &StatusOptions,
status_func: &dyn FnMut(&'_ str, &'_ Status) -> Result<(), Error<'static>>,
) -> Result<Option<Revnum>, Error<'static>> {
with_tmp_pool(|pool| {
let path_cstr = crate::dirent::canonicalize_path_or_url(path)?;
let changelist_cstrings: Vec<std::ffi::CString> =
if let Some(changelists) = &options.changelists {
changelists
.iter()
.map(|cl| std::ffi::CString::new(cl.as_str()).unwrap())
.collect()
} else {
Vec::new()
};
let mut cl = apr::tables::TypedArray::new(pool, changelist_cstrings.len() as i32);
for changelist in &changelist_cstrings {
cl.push(changelist.as_ptr() as *mut std::ffi::c_void);
}
unsafe {
let status_func = Box::into_raw(Box::new(status_func));
let mut revnum = 0;
let rev_c: crate::svn_opt_revision_t = options.revision.into();
let err = svn_client_status6(
&mut revnum,
self.ptr,
path_cstr.as_ptr(),
&rev_c,
options.depth.into(),
options.get_all.into(),
options.check_out_of_date.into(),
options.check_working_copy.into(),
options.no_ignore.into(),
options.ignore_externals.into(),
options.depth_as_sticky.into(),
cl.as_ptr(),
Some(wrap_status_func),
status_func as *mut std::ffi::c_void,
pool.as_mut_ptr(),
);
let _ = Box::from_raw(status_func);
Error::from_raw(err)?;
Ok(Revnum::from_raw(revnum))
}
})
}
pub fn log(
&mut self,
targets: &[&str],
revision_ranges: &[RevisionRange],
options: &LogOptions,
log_entry_receiver: &dyn FnMut(&LogEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| unsafe {
let target_cstrings: Vec<std::ffi::CString> = targets
.iter()
.map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
.collect();
let mut ps = apr::tables::TypedArray::new(pool, targets.len() as i32);
for target in &target_cstrings {
ps.push(target.as_ptr() as *mut std::ffi::c_void);
}
let mut rrs =
apr::tables::TypedArray::<*mut subversion_sys::svn_opt_revision_range_t>::new(
pool,
revision_ranges.len() as i32,
);
for revision_range in revision_ranges {
rrs.push(revision_range.to_c(pool));
}
let revprop_cstrings: Vec<std::ffi::CString> = options
.revprops
.as_ref()
.map(|rps| {
rps.iter()
.map(|r| std::ffi::CString::new(r.as_str()).unwrap())
.collect()
})
.unwrap_or_default();
let mut rps_array;
let rps_ptr = if options.revprops.is_some() {
rps_array = apr::tables::TypedArray::new(pool, revprop_cstrings.len() as i32);
for revprop in &revprop_cstrings {
rps_array.push(revprop.as_ptr() as *mut std::ffi::c_void);
}
rps_array.as_ptr()
} else {
std::ptr::null()
};
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let err = svn_client_log5(
ps.as_ptr(),
&peg_rev_c,
rrs.as_ptr(),
options.limit.unwrap_or(0),
options.discover_changed_paths.into(),
options.strict_node_history.into(),
options.include_merged_revisions.into(),
rps_ptr,
Some(crate::wrap_log_entry_receiver),
&log_entry_receiver as *const _ as *mut std::ffi::c_void,
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
})
}
pub fn log_with_control<F>(
&mut self,
targets: &[&str],
revision_ranges: &[RevisionRange],
options: &LogOptions,
mut receiver: F,
) -> Result<(), Error<'static>>
where
F: FnMut(&LogEntry) -> ControlFlow<()>,
{
self.log(
targets,
revision_ranges,
options,
&|entry| match receiver(entry) {
ControlFlow::Continue(()) => Ok(()),
ControlFlow::Break(()) => {
Err(Error::with_raw_status(
subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32,
None,
"Log iteration stopped by user",
))
}
},
)
}
pub fn mergeinfo_log(
&mut self,
target_path_or_url: &str,
source_path_or_url: &str,
options: &MergeinfoLogOptions,
log_entry_receiver: &mut dyn FnMut(&crate::LogEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let target_c = crate::dirent::canonicalize_path_or_url(target_path_or_url).unwrap();
let source_c = crate::dirent::canonicalize_path_or_url(source_path_or_url).unwrap();
let revprop_cstrings: Vec<std::ffi::CString> = options
.revprops
.iter()
.map(|s| std::ffi::CString::new(s.as_str()).unwrap())
.collect();
let mut rps = apr::tables::TypedArray::new(pool, revprop_cstrings.len() as i32);
for revprop in &revprop_cstrings {
rps.push(revprop.as_ptr() as *mut std::ffi::c_void);
}
let target_peg_rev_c: crate::svn_opt_revision_t = options.target_peg_revision.into();
let source_peg_rev_c: crate::svn_opt_revision_t = options.source_peg_revision.into();
let source_start_rev_c: crate::svn_opt_revision_t =
options.source_start_revision.into();
let source_end_rev_c: crate::svn_opt_revision_t = options.source_end_revision.into();
let err = unsafe {
subversion_sys::svn_client_mergeinfo_log2(
options.finding_merged as i32,
target_c.as_ptr(),
&target_peg_rev_c,
source_c.as_ptr(),
&source_peg_rev_c,
&source_start_rev_c,
&source_end_rev_c,
Some(crate::wrap_log_entry_receiver),
&log_entry_receiver as *const _ as *mut std::ffi::c_void,
options.discover_changed_paths as i32,
options.depth.into(),
rps.as_ptr(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})
}
pub fn mergeinfo_get_merged(
&mut self,
path_or_url: &str,
peg_revision: &Revision,
) -> Result<Option<crate::mergeinfo::Mergeinfo>, Error<'_>> {
let pool = apr::Pool::new();
let path_c = crate::dirent::canonicalize_path_or_url(path_or_url)?;
let mut mergeinfo_hash: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
let peg_rev: subversion_sys::svn_opt_revision_t = (*peg_revision).into();
let err = unsafe {
subversion_sys::svn_client_mergeinfo_get_merged(
&mut mergeinfo_hash,
path_c.as_ptr(),
&peg_rev,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if mergeinfo_hash.is_null() {
return Ok(None);
}
unsafe {
Ok(Some(crate::mergeinfo::Mergeinfo::from_ptr_and_pool(
mergeinfo_hash,
pool,
)))
}
}
pub fn iter_logs(
&mut self,
targets: &[&str],
revision_ranges: &[RevisionRange],
options: &LogOptions,
) -> LogIterator<'_> {
let (tx, rx) = std::sync::mpsc::sync_channel::<Result<OwnedLogEntry, Error<'static>>>(4);
let targets: Vec<String> = targets.iter().map(|s| s.to_string()).collect();
let revision_ranges = revision_ranges.to_vec();
let options = options.clone();
let ctx_addr = self as *mut Context as usize;
let handle = std::thread::spawn(move || {
let ctx = unsafe { &mut *(ctx_addr as *mut Context) };
let target_refs: Vec<&str> = targets.iter().map(|s| s.as_str()).collect();
let result = ctx.log(&target_refs, &revision_ranges, &options, &|entry| {
let owned = OwnedLogEntry::from_log_entry(entry);
if tx.send(Ok(owned)).is_err() {
return Err(Error::with_raw_status(
subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32,
None,
"Log iteration cancelled",
));
}
Ok(())
});
if let Err(e) = result {
if e.raw_apr_err() != subversion_sys::svn_errno_t_SVN_ERR_CANCELLED as i32 {
let _ = tx.send(Err(e));
}
}
});
LogIterator {
rx,
handle: Some(handle),
_phantom: std::marker::PhantomData,
}
}
pub fn args_to_target_array(
&mut self,
mut os: apr::getopt::Getopt,
known_targets: &[&str],
keep_last_origpath_on_truepath_collision: bool,
) -> Result<Vec<String>, crate::Error<'_>> {
let pool = apr::pool::Pool::new();
let known_targets = known_targets
.iter()
.map(|s| std::ffi::CString::new(*s).unwrap())
.collect::<Vec<_>>();
let mut targets = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_client_args_to_target_array2(
&mut targets,
os.as_mut_ptr(),
{
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, 10);
for s in known_targets {
array.push(s.as_ptr());
}
array.as_ptr()
},
self.ptr,
keep_last_origpath_on_truepath_collision.into(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let targets = unsafe { apr::tables::TypedArray::<*const i8>::from_ptr(targets) };
Ok(targets
.iter()
.map(|s| unsafe { std::ffi::CStr::from_ptr(*s as *const i8) })
.map(|s| s.to_str().unwrap().to_owned())
.collect::<Vec<_>>())
}
pub fn vacuum(
&mut self,
path: impl AsCanonicalDirent,
options: &VacuumOptions,
) -> Result<(), Error<'static>> {
let path = crate::dirent::to_absolute_cstring(path)?;
with_tmp_pool(|pool| unsafe {
let err = svn_client_vacuum(
path.as_ptr(),
options.remove_unversioned_items.into(),
options.remove_ignored_items.into(),
options.fix_recorded_timestamps.into(),
options.vacuum_pristines.into(),
options.include_externals.into(),
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)
})
}
pub fn cleanup(
&mut self,
path: impl AsCanonicalDirent,
options: &CleanupOptions,
) -> Result<(), Error<'static>> {
let path = crate::dirent::to_absolute_cstring(path)?;
with_tmp_pool(|pool| unsafe {
let err = svn_client_cleanup2(
path.as_ptr(),
options.break_locks.into(),
options.fix_recorded_timestamps.into(),
options.clear_dav_cache.into(),
options.vacuum_pristines.into(),
options.include_externals.into(),
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)
})
}
pub fn conflict_get(
&mut self,
local_abspath: impl AsCanonicalDirent,
) -> Result<Conflict, Error<'static>> {
let pool = apr::Pool::new();
let local_abspath = local_abspath.as_canonical_dirent()?;
let mut conflict: *mut subversion_sys::svn_client_conflict_t = std::ptr::null_mut();
with_tmp_pool(|tmp_pool| unsafe {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let err = svn_client_conflict_get(
&mut conflict,
path_cstr.as_ptr(),
self.ptr,
pool.as_mut_ptr(), tmp_pool.as_mut_ptr(), );
Error::from_raw(err)?;
Ok(Conflict::from_ptr_and_pool(conflict, pool))
})
}
pub fn conflict_walk<F>(
&mut self,
local_abspath: impl AsCanonicalDirent,
depth: crate::Depth,
mut callback: F,
) -> Result<(), Error<'static>>
where
F: FnMut(Conflict) -> Result<(), Error<'static>>,
{
let local_abspath = local_abspath.as_canonical_dirent()?;
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
struct WalkBaton<'a, F: FnMut(Conflict) -> Result<(), Error<'static>>> {
callback: &'a mut F,
error: Option<Error<'static>>,
}
unsafe extern "C" fn walk_func<F: FnMut(Conflict) -> Result<(), Error<'static>>>(
baton: *mut std::ffi::c_void,
conflict: *mut subversion_sys::svn_client_conflict_t,
scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let baton = &mut *(baton as *mut WalkBaton<F>);
let pool = apr::Pool::from_raw(scratch_pool);
let conflict_obj = Conflict {
ptr: conflict,
pool,
_phantom: std::marker::PhantomData,
};
match (baton.callback)(conflict_obj) {
Ok(()) => std::ptr::null_mut(),
Err(e) => {
baton.error = Some(e);
std::ptr::null_mut()
}
}
}
let mut baton = WalkBaton {
callback: &mut callback,
error: None,
};
with_tmp_pool(|pool| unsafe {
let err = subversion_sys::svn_client_conflict_walk(
path_cstr.as_ptr(),
depth.into(),
Some(walk_func::<F>),
&mut baton as *mut WalkBaton<F> as *mut std::ffi::c_void,
self.ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
if let Some(e) = baton.error {
return Err(e);
}
Ok(())
})
}
pub fn cat(
&mut self,
path_or_url: &str,
stream: &mut dyn std::io::Write,
options: &CatOptions,
) -> Result<HashMap<String, Vec<u8>>, Error<'_>> {
let path_or_url = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
let mut s = crate::io::wrap_write(stream)?;
with_tmp_pool(|result_pool| {
with_tmp_pool(|scratch_pool| unsafe {
let mut props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let rev_c: crate::svn_opt_revision_t = options.revision.into();
let err = subversion_sys::svn_client_cat3(
&mut props,
s.as_mut_ptr(),
path_or_url.as_ptr(),
&peg_rev_c,
&rev_c,
options.expand_keywords.into(),
self.as_mut_ptr(),
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
Error::from_raw(err)?;
let prop_hash = crate::props::PropHash::from_ptr(props);
Ok(prop_hash.to_hashmap())
})
})
}
pub fn lock(
&mut self,
targets: &[&str],
comment: &str,
steal_lock: bool,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let targets = targets
.iter()
.map(|s| std::ffi::CString::new(*s).unwrap())
.collect::<Vec<_>>();
let mut targets_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
for target in targets.iter() {
targets_array.push(target.as_ptr());
}
let comment = std::ffi::CString::new(comment).unwrap();
unsafe {
let err = subversion_sys::svn_client_lock(
targets_array.as_ptr(),
comment.as_ptr(),
steal_lock.into(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
}
})
}
pub fn unlock(&mut self, targets: &[&str], break_lock: bool) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let targets = targets
.iter()
.map(|s| std::ffi::CString::new(*s).unwrap())
.collect::<Vec<_>>();
let mut targets_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
for target in targets.iter() {
targets_array.push(target.as_ptr());
}
unsafe {
let err = subversion_sys::svn_client_unlock(
targets_array.as_ptr(),
break_lock.into(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
}
})
}
pub fn get_wc_root(
&mut self,
path: impl AsCanonicalDirent,
) -> Result<std::path::PathBuf, Error<'static>> {
let path = path.as_canonical_dirent()?;
let mut wc_root: *const i8 = std::ptr::null();
with_tmp_pool(|tmp_pool| unsafe {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let err = subversion_sys::svn_client_get_wc_root(
&mut wc_root,
path_cstr.as_ptr(),
self.as_mut_ptr(),
tmp_pool.as_mut_ptr(),
tmp_pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(std::ffi::CStr::from_ptr(wc_root).to_str().unwrap().into())
})
}
pub fn min_max_revisions(
&mut self,
local_abspath: impl AsCanonicalDirent,
committed: bool,
) -> Result<(Revnum, Revnum), Error<'static>> {
let _scratch_pool = apr::pool::Pool::new();
let local_abspath = local_abspath.as_canonical_dirent()?;
let mut min_revision: subversion_sys::svn_revnum_t = 0;
let mut max_revision: subversion_sys::svn_revnum_t = 0;
with_tmp_pool(|tmp_pool| unsafe {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let err = subversion_sys::svn_client_min_max_revisions(
&mut min_revision,
&mut max_revision,
path_cstr.as_ptr(),
committed as i32,
self.as_mut_ptr(),
tmp_pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok((
Revnum::from_raw(min_revision).unwrap(),
Revnum::from_raw(max_revision).unwrap(),
))
})
}
pub fn url_from_path(&mut self, path: impl AsCanonicalUri) -> Result<String, Error<'static>> {
let path = path.as_canonical_uri()?;
with_tmp_pool(|pool| unsafe {
let mut url: *const i8 = std::ptr::null();
let path_cstr = std::ffi::CString::new(path.as_str())?;
let err = subversion_sys::svn_client_url_from_path2(
&mut url,
path_cstr.as_ptr(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(std::ffi::CStr::from_ptr(url).to_str().unwrap().into())
})
}
pub fn get_repos_root(
&mut self,
path_or_url: &str,
) -> Result<(String, String), Error<'static>> {
let path_or_url = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
with_tmp_pool(|pool| unsafe {
let mut repos_root: *const i8 = std::ptr::null();
let mut repos_uuid: *const i8 = std::ptr::null();
let err = subversion_sys::svn_client_get_repos_root(
&mut repos_root,
&mut repos_uuid,
path_or_url.as_ptr(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok((
std::ffi::CStr::from_ptr(repos_root)
.to_str()
.unwrap()
.into(),
std::ffi::CStr::from_ptr(repos_uuid)
.to_str()
.unwrap()
.into(),
))
})
}
#[cfg(feature = "ra")]
pub fn open_raw_session(
&mut self,
url: &str,
wri_path: &std::path::Path,
) -> Result<crate::ra::Session<'_>, Error<'_>> {
let url = std::ffi::CString::new(url).unwrap();
let wri_path = crate::dirent::to_absolute_cstring(wri_path)?;
let pool = apr::Pool::new();
unsafe {
let scratch_pool = Pool::default();
let mut session: *mut subversion_sys::svn_ra_session_t = std::ptr::null_mut();
let err = subversion_sys::svn_client_open_ra_session2(
&mut session,
url.as_ptr(),
wri_path.as_ptr(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(crate::ra::Session::from_ptr_and_pool(session, pool))
}
}
pub fn info(
&mut self,
abspath_or_url: &str,
options: &InfoOptions,
receiver: &dyn FnMut(&std::path::Path, &Info) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
validate_absolute_path_or_url(abspath_or_url)?;
with_tmp_pool(|pool| {
let abspath_or_url = std::ffi::CString::new(abspath_or_url).unwrap();
let changelists = options.changelists.as_ref().map(|cl| {
cl.iter()
.map(|cl| std::ffi::CString::new(cl.as_str()).unwrap())
.collect::<Vec<_>>()
});
let changelists = changelists.as_ref().map(|cl| {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
for item in cl.iter() {
array.push(item.as_ptr());
}
array
});
let mut receiver = receiver;
let receiver = &mut receiver as *mut _ as *mut std::ffi::c_void;
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let rev_c: crate::svn_opt_revision_t = options.revision.into();
unsafe {
let err = subversion_sys::svn_client_info4(
abspath_or_url.as_ptr(),
&peg_rev_c,
&rev_c,
options.depth.into(),
options.fetch_excluded as i32,
options.fetch_actual_only as i32,
options.include_externals as i32,
changelists.map_or(std::ptr::null(), |cl| cl.as_ptr()),
Some(wrap_info_receiver2),
receiver,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok(())
}
})
}
pub fn blame(
&mut self,
path_or_url: &str,
options: &BlameOptions,
receiver: &mut dyn FnMut(BlameInfo) -> Result<(), Error<'static>>,
) -> Result<(Revnum, Revnum), Error<'static>> {
with_tmp_pool(|pool| {
let path_or_url = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
let diff_file_options =
unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
if !options.diff_options.is_empty() {
let diff_options_cstrings: Vec<std::ffi::CString> = options
.diff_options
.iter()
.map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
.collect();
let mut diff_opts_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
for opt in diff_options_cstrings.iter() {
diff_opts_array.push(opt.as_ptr());
}
let parse_err = unsafe {
subversion_sys::svn_diff_file_options_parse(
diff_file_options,
diff_opts_array.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(parse_err)?;
}
let mut start_revnum: subversion_sys::svn_revnum_t = 0;
let mut end_revnum: subversion_sys::svn_revnum_t = 0;
let receiver_baton = receiver as *mut _ as *mut std::ffi::c_void;
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let start_rev_c: crate::svn_opt_revision_t = options.start_revision.into();
let end_rev_c: crate::svn_opt_revision_t = options.end_revision.into();
unsafe {
let err = subversion_sys::svn_client_blame6(
&mut start_revnum,
&mut end_revnum,
path_or_url.as_ptr(),
&peg_rev_c,
&start_rev_c,
&end_rev_c,
diff_file_options,
options.ignore_mime_type as subversion_sys::svn_boolean_t,
options.include_merged_revisions as subversion_sys::svn_boolean_t,
Some(blame_receiver_wrapper),
receiver_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
Ok((
Revnum::from_raw(start_revnum).unwrap(),
Revnum::from_raw(end_revnum).unwrap(),
))
}
})
}
pub fn copy(
&mut self,
sources: &[(&str, Option<Revision>)],
dst_path: &str,
options: &mut CopyOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path_cstrings: Vec<std::ffi::CString> = sources
.iter()
.map(|(path, _)| crate::dirent::canonicalize_path_or_url(path).unwrap())
.collect();
let sources_array = sources
.iter()
.zip(path_cstrings.iter())
.map(|((_, rev), path_c)| {
let src: *mut subversion_sys::svn_client_copy_source_t = pool.calloc();
let revision: *mut subversion_sys::svn_opt_revision_t = pool.calloc();
let peg_revision: *mut subversion_sys::svn_opt_revision_t = pool.calloc();
unsafe {
(*src).path = path_c.as_ptr();
*revision = rev
.as_ref()
.map(|r| (*r).into())
.unwrap_or(Revision::Head.into());
(*src).revision = revision;
*peg_revision = Revision::Head.into();
(*src).peg_revision = peg_revision;
}
src
})
.collect::<Vec<_>>();
let mut sources_apr_array = apr::tables::TypedArray::<
*const subversion_sys::svn_client_copy_source_t,
>::new(pool, sources_array.len() as i32);
for src in sources_array.iter() {
sources_apr_array.push(*src as *const _);
}
let dst_c = crate::dirent::canonicalize_path_or_url(dst_path)?;
let externals_hash = options.externals_to_pin.as_ref().map(|ext| {
let svn_strings: Vec<_> = ext
.iter()
.map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
.collect();
apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
)
});
let revprop_hash = options.revprop_table.as_ref().map(|rp| {
let svn_strings: Vec<_> = rp
.iter()
.map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
.collect();
apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
)
});
let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
{
(
Some(crate::wrap_commit_callback2 as _),
cb as *mut _ as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let err = unsafe {
subversion_sys::svn_client_copy7(
sources_apr_array.as_ptr(),
dst_c.as_ptr(),
options.copy_as_child as i32,
options.make_parents as i32,
options.ignore_externals as i32,
options.metadata_only as i32,
options.pin_externals as i32,
externals_hash
.as_ref()
.map_or(std::ptr::null_mut(), |h| h.as_ptr()),
revprop_hash
.as_ref()
.map_or(std::ptr::null_mut(), |h| h.as_ptr()),
callback_func,
callback_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn mkdir_multiple(
&mut self,
paths: &[&str],
options: &mut MkdirOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let paths_c: Vec<_> = paths
.iter()
.map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
.collect();
let mut paths_array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
for path in paths_c.iter() {
paths_array.push(path.as_ptr());
}
let revprop_hash = options.revprop_table.as_ref().map(|revprops| {
let svn_strings: Vec<_> = revprops
.iter()
.map(|(k, v)| (k.as_str(), crate::string::BStr::from_bytes(v, pool)))
.collect();
apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
)
});
let revprop_ptr = revprop_hash
.as_ref()
.map_or(std::ptr::null_mut(), |h| unsafe { h.as_ptr() as *mut _ });
let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback
{
(
Some(crate::wrap_commit_callback2 as _),
cb as *mut _ as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let err = unsafe {
subversion_sys::svn_client_mkdir4(
paths_array.as_ptr(),
options.make_parents as i32,
revprop_ptr,
callback_func,
callback_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn propget(
&mut self,
propname: &str,
target: &str,
options: &PropGetOptions,
actual_revnum: Option<&mut Revnum>,
) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
with_tmp_pool(|pool| {
let propname_c = std::ffi::CString::new(propname).unwrap();
let target_c = crate::dirent::canonicalize_path_or_url(target)?;
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
let mut props = std::ptr::null_mut();
let mut actual_rev = actual_revnum.as_ref().map_or(0, |r| r.0);
let peg_rev_c: crate::svn_opt_revision_t = options.peg_revision.into();
let rev_c: crate::svn_opt_revision_t = options.revision.into();
let err = unsafe {
subversion_sys::svn_client_propget5(
&mut props,
std::ptr::null_mut(), propname_c.as_ptr(),
target_c.as_ptr(),
&peg_rev_c,
&rev_c,
if actual_revnum.is_some() {
&mut actual_rev
} else {
std::ptr::null_mut()
},
options.depth.into(),
changelists_array,
self.as_mut_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
svn_result(err)?;
if let Some(revnum) = actual_revnum {
*revnum = Revnum(actual_rev);
}
if props.is_null() {
return Ok(std::collections::HashMap::new());
}
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
Ok(prop_hash.to_hashmap())
})
}
pub fn propget_with_inherited(
&mut self,
propname: &str,
target: &str,
options: &PropGetOptions,
actual_revnum: Option<&mut Revnum>,
) -> Result<
(
std::collections::HashMap<String, Vec<u8>>,
Vec<InheritedPropItem>,
),
Error<'_>,
> {
with_tmp_pool(|pool| {
let propname_c = std::ffi::CString::new(propname).unwrap();
let target_c = crate::dirent::canonicalize_path_or_url(target).unwrap();
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
let mut props = std::ptr::null_mut();
let mut inherited_props = std::ptr::null_mut();
let mut actual_rev = actual_revnum.as_ref().map_or(0, |r| r.0);
let err = unsafe {
subversion_sys::svn_client_propget5(
&mut props,
&mut inherited_props,
propname_c.as_ptr(),
target_c.as_ptr(),
&options.peg_revision.into(),
&options.revision.into(),
if actual_revnum.is_some() {
&mut actual_rev
} else {
std::ptr::null_mut()
},
options.depth.into(),
changelists_array,
self.as_mut_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
svn_result(err)?;
if let Some(revnum) = actual_revnum {
*revnum = Revnum(actual_rev);
}
let properties = if props.is_null() {
std::collections::HashMap::new()
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
prop_hash.to_hashmap()
};
let inherited = if inherited_props.is_null() {
Vec::new()
} else {
let array = unsafe {
apr::tables::TypedArray::<*mut subversion_sys::svn_prop_inherited_item_t>::from_ptr(inherited_props)
};
let mut result = Vec::new();
for item_ptr in array.iter() {
if item_ptr.is_null() {
continue;
}
unsafe {
let item = *item_ptr;
let path = 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(InheritedPropItem {
path_or_url: path,
properties: props,
});
}
}
result
};
Ok((properties, inherited))
})
}
pub fn propset(
&mut self,
propname: &str,
propval: Option<&[u8]>,
target: &str,
options: &PropSetOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let propname_c = std::ffi::CString::new(propname).unwrap();
let target_c = crate::dirent::canonicalize_path_or_url(target).unwrap();
let mut targets_array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
targets_array.push(target_c.as_ptr());
let propval_ptr = propval.map(|val| crate::svn_string_ncreate(val, pool));
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
let err = unsafe {
subversion_sys::svn_client_propset_local(
propname_c.as_ptr(),
propval_ptr
.map(|p| p as *const _)
.unwrap_or(std::ptr::null()),
targets_array.as_ptr(),
options.depth.into(),
options.skip_checks as i32,
changelists_array,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
svn_result(err)
})
}
pub fn propset_remote(
&mut self,
propname: &str,
propval: Option<&[u8]>,
url: &str,
options: &mut PropSetRemoteOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let propname_c = std::ffi::CString::new(propname).unwrap();
let url_c = std::ffi::CString::new(url).unwrap();
let propval_ptr = propval.map(|val| crate::svn_string_ncreate(val, pool));
let revprop_hash = options.revprop_table.as_ref().map(|rp| {
let svn_strings: Vec<_> = rp
.iter()
.map(|(k, v)| (k.as_str(), crate::string::BStr::from_str(v, pool)))
.collect();
apr::hash::Hash::from_iter(
pool,
svn_strings
.iter()
.map(|(k, v)| (k.as_bytes(), v.as_ptr() as *mut std::ffi::c_void)),
)
});
unsafe {
let (callback_func, callback_baton) = if let Some(ref mut cb) =
options.commit_callback
{
(
Some(crate::wrap_commit_callback2 as unsafe extern "C" fn(_, _, _) -> _),
Box::into_raw(Box::new(cb)) as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let err = subversion_sys::svn_client_propset_remote(
propname_c.as_ptr(),
propval_ptr
.map(|p| p as *const _)
.unwrap_or(std::ptr::null()),
url_c.as_ptr(),
options.skip_checks as i32,
options.base_revision_for_url.0,
revprop_hash
.as_ref()
.map_or(std::ptr::null(), |h| h.as_ptr()),
callback_func,
callback_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)
}
})
}
pub fn proplist_all(
&mut self,
target: &str,
options: &ProplistOptions,
receiver: &mut dyn FnMut(
&str,
std::collections::HashMap<String, Vec<u8>>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let target_c = crate::dirent::canonicalize_path_or_url(target).unwrap();
let changelists = options.changelists.map(|cl| {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
for item in cl.iter() {
let item_c = std::ffi::CString::new(*item).unwrap();
array.push(item_c.as_ptr());
}
array
});
extern "C" fn proplist_receiver(
baton: *mut std::ffi::c_void,
path: *const i8,
props: *mut apr_sys::apr_hash_t,
_inherited_props: *mut apr_sys::apr_array_header_t,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let receiver = unsafe {
&mut *(baton
as *mut &mut dyn FnMut(
&str,
std::collections::HashMap<String, Vec<u8>>,
)
-> Result<(), Error<'static>>)
};
let path_str = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() };
let prop_hash_wrapper = unsafe { crate::props::PropHash::from_ptr(props) };
let prop_hash = prop_hash_wrapper.to_hashmap();
match receiver(path_str, prop_hash) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => unsafe { e.detach() },
}
}
let receiver_ptr = receiver as *mut _ as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_client_proplist4(
target_c.as_ptr(),
&options.peg_revision.into(),
&options.revision.into(),
options.depth.into(),
changelists.map_or(std::ptr::null(), |cl| cl.as_ptr()),
options.get_target_inherited_props as i32,
Some(proplist_receiver),
receiver_ptr,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn diff(
&mut self,
path_or_url1: &str,
revision1: &Revision,
path_or_url2: &str,
revision2: &Revision,
relative_to_dir: Option<&str>,
outstream: &mut crate::io::Stream,
errstream: &mut crate::io::Stream,
options: &DiffOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path1_c = crate::dirent::canonicalize_path_or_url(path_or_url1).unwrap();
let path2_c = crate::dirent::canonicalize_path_or_url(path_or_url2).unwrap();
let header_encoding_c =
std::ffi::CString::new(options.header_encoding.as_str()).unwrap();
let diff_options_c: Vec<_> = options
.diff_options
.iter()
.map(|o| std::ffi::CString::new(o.as_str()).unwrap())
.collect();
let mut diff_options_array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
for opt in diff_options_c.iter() {
diff_options_array.push(opt.as_ptr());
}
let relative_to_dir_c = relative_to_dir
.map(|d| crate::dirent::to_absolute_cstring(d))
.transpose()?;
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
let err = unsafe {
subversion_sys::svn_client_diff7(
diff_options_array.as_ptr(),
path1_c.as_ptr(),
&(*revision1).into(),
path2_c.as_ptr(),
&(*revision2).into(),
relative_to_dir_c
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
options.depth.into(),
options.ignore_ancestry as i32,
options.no_diff_added as i32,
options.no_diff_deleted as i32,
options.show_copies_as_adds as i32,
options.ignore_content_type as i32,
options.ignore_properties as i32,
options.properties_only as i32,
options.use_git_diff_format as i32,
options.pretty_print_mergeinfo as i32,
header_encoding_c.as_ptr(),
outstream.as_mut_ptr(),
errstream.as_mut_ptr(),
changelists_array,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
svn_result(err)
})
}
pub fn diff_peg(
&mut self,
path_or_url: &str,
peg_revision: &Revision,
start_revision: &Revision,
end_revision: &Revision,
relative_to_dir: Option<&str>,
outstream: &mut crate::io::Stream,
errstream: &mut crate::io::Stream,
options: &DiffOptions,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path_c = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
let header_encoding_c =
std::ffi::CString::new(options.header_encoding.as_str()).unwrap();
let relative_to_dir_c = relative_to_dir
.map(|d| crate::dirent::to_absolute_cstring(d))
.transpose()?;
let diff_file_options =
unsafe { subversion_sys::svn_diff_file_options_create(pool.as_mut_ptr()) };
if !options.diff_options.is_empty() {
let diff_options_cstrings: Vec<std::ffi::CString> = options
.diff_options
.iter()
.map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
.collect();
let mut diff_opts_array = apr::tables::TypedArray::<*const i8>::new(pool, 10);
for opt in diff_options_cstrings.iter() {
diff_opts_array.push(opt.as_ptr());
}
let parse_err = unsafe {
subversion_sys::svn_diff_file_options_parse(
diff_file_options,
diff_opts_array.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(parse_err)?;
}
let diff_options_array = unsafe {
subversion_sys::svn_cstring_split(
std::ptr::null(),
std::ptr::null(),
0,
pool.as_mut_ptr(),
)
};
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
let err = unsafe {
subversion_sys::svn_client_diff_peg7(
diff_options_array,
path_c.as_ptr(),
&(*peg_revision).into(),
&(*start_revision).into(),
&(*end_revision).into(),
relative_to_dir_c
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
options.depth.into(),
options.ignore_ancestry as i32,
options.no_diff_added as i32,
options.no_diff_deleted as i32,
options.show_copies_as_adds as i32,
options.ignore_content_type as i32,
options.ignore_properties as i32,
options.properties_only as i32,
options.use_git_diff_format as i32,
options.pretty_print_mergeinfo as i32,
header_encoding_c.as_ptr(),
outstream.as_mut_ptr(),
errstream.as_mut_ptr(),
changelists_array,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
svn_result(err)
})
}
pub fn diff_summarize(
&mut self,
path_or_url1: &str,
revision1: &Revision,
path_or_url2: &str,
revision2: &Revision,
options: &DiffSummarizeOptions,
summarize_func: &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path1_c = crate::dirent::canonicalize_path_or_url(path_or_url1).unwrap();
let path2_c = crate::dirent::canonicalize_path_or_url(path_or_url2).unwrap();
let (changelists_array, _list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
extern "C" fn summarize_callback(
diff: *const subversion_sys::svn_client_diff_summarize_t,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let callback = &mut *(baton
as *mut &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>);
let summary = DiffSummary::from_raw(diff);
match callback(summary) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => e.detach(),
}
}
}
let callback_baton = &summarize_func as *const _ as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_client_diff_summarize2(
path1_c.as_ptr(),
&(*revision1).into(),
path2_c.as_ptr(),
&(*revision2).into(),
options.depth.into(),
options.ignore_ancestry as i32,
changelists_array,
Some(summarize_callback as unsafe extern "C" fn(_, _, _) -> _),
callback_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn diff_summarize_peg(
&mut self,
path_or_url: &str,
peg_revision: &Revision,
start_revision: &Revision,
end_revision: &Revision,
options: &DiffSummarizeOptions,
summarize_func: &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path_c = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
extern "C" fn summarize_callback(
diff: *const subversion_sys::svn_client_diff_summarize_t,
baton: *mut std::ffi::c_void,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let callback = &mut *(baton
as *mut &mut dyn FnMut(DiffSummary) -> Result<(), Error<'static>>);
let summary = DiffSummary::from_raw(diff);
match callback(summary) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => e.detach(),
}
}
}
let callback_baton = &summarize_func as *const _ as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_client_diff_summarize_peg2(
path_c.as_ptr(),
&(*peg_revision).into(),
&(*start_revision).into(),
&(*end_revision).into(),
options.depth.into(),
options.ignore_ancestry as i32,
changelists_array,
Some(summarize_callback as unsafe extern "C" fn(_, _, _) -> _),
callback_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
svn_result(err)
})
}
pub fn list(
&mut self,
path_or_url: &str,
options: &ListOptions,
list_func: &mut dyn FnMut(
&str,
&crate::DirEntry,
Option<&crate::Lock>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path_or_url_c = crate::dirent::canonicalize_path_or_url(path_or_url).unwrap();
let pattern_cstrings: Vec<std::ffi::CString> = options
.patterns
.as_ref()
.map(|pats| {
pats.iter()
.map(|p| std::ffi::CString::new(p.as_str()).unwrap())
.collect()
})
.unwrap_or_default();
let patterns = if !pattern_cstrings.is_empty() {
let mut array = apr::tables::TypedArray::<*const i8>::new(pool, 0);
for pattern_c in pattern_cstrings.iter() {
array.push(pattern_c.as_ptr());
}
Some(array)
} else if options.patterns.is_some() {
Some(apr::tables::TypedArray::<*const i8>::new(pool, 0))
} else {
None
};
extern "C" fn list_receiver(
baton: *mut std::ffi::c_void,
path: *const i8,
dirent: *const subversion_sys::svn_dirent_t,
lock: *const subversion_sys::svn_lock_t,
_abs_path: *const i8,
_external_parent_url: *const i8,
_external_target: *const i8,
scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let list_func = unsafe {
&mut *(baton
as *mut &mut dyn FnMut(
&str,
&crate::DirEntry,
Option<&crate::Lock>,
)
-> Result<(), Error<'static>>)
};
let path_str = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() };
let dirent = crate::DirEntry::from_raw(dirent as *mut _);
let lock = if lock.is_null() {
None
} else {
let pool_handle = unsafe { apr::PoolHandle::from_borrowed_raw(scratch_pool) };
Some(crate::Lock::from_raw(lock as *mut _, pool_handle))
};
match list_func(path_str, &dirent, lock.as_ref()) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => unsafe { e.detach() },
}
}
let list_func_ptr = &list_func as *const _ as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_client_list4(
path_or_url_c.as_ptr(),
&options.peg_revision.into(),
&options.revision.into(),
patterns.map_or(std::ptr::null(), |p| p.as_ptr()),
options.depth.into(),
options.dirent_fields,
options.fetch_locks as i32,
options.include_externals as i32,
Some(list_receiver),
list_func_ptr,
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
pub fn resolve(
&mut self,
path: &str,
depth: Depth,
conflict_choice: crate::ConflictChoice,
) -> Result<(), Error<'static>> {
with_tmp_pool(|pool| {
let path_c = crate::dirent::canonicalize_path_or_url(path).unwrap();
let err = unsafe {
subversion_sys::svn_client_resolve(
path_c.as_ptr(),
depth.into(),
conflict_choice.into(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
svn_result(err)
})
}
}
pub struct DiffBuilder<'a> {
ctx: &'a mut Context,
path1: String,
revision1: Revision,
path2: String,
revision2: Revision,
depth: Depth,
diff_options: Vec<String>,
relative_to_dir: Option<String>,
ignore_ancestry: bool,
no_diff_added: bool,
no_diff_deleted: bool,
show_copies_as_adds: bool,
ignore_content_type: bool,
ignore_properties: bool,
properties_only: bool,
use_git_diff_format: bool,
header_encoding: String,
changelists: Option<Vec<String>>,
}
impl<'a> DiffBuilder<'a> {
pub fn new(
ctx: &'a mut Context,
path1: impl Into<String>,
revision1: Revision,
path2: impl Into<String>,
revision2: Revision,
) -> Self {
Self {
ctx,
path1: path1.into(),
revision1,
path2: path2.into(),
revision2,
depth: Depth::Infinity,
diff_options: Vec::new(),
relative_to_dir: None,
ignore_ancestry: false,
no_diff_added: false,
no_diff_deleted: false,
show_copies_as_adds: false,
ignore_content_type: false,
ignore_properties: false,
properties_only: false,
use_git_diff_format: false,
header_encoding: "UTF-8".to_string(),
changelists: None,
}
}
pub fn depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn diff_options(mut self, options: Vec<String>) -> Self {
self.diff_options = options;
self
}
pub fn relative_to_dir(mut self, dir: impl Into<String>) -> Self {
self.relative_to_dir = Some(dir.into());
self
}
pub fn ignore_ancestry(mut self, ignore: bool) -> Self {
self.ignore_ancestry = ignore;
self
}
pub fn no_diff_added(mut self, no_diff: bool) -> Self {
self.no_diff_added = no_diff;
self
}
pub fn no_diff_deleted(mut self, no_diff: bool) -> Self {
self.no_diff_deleted = no_diff;
self
}
pub fn show_copies_as_adds(mut self, show: bool) -> Self {
self.show_copies_as_adds = show;
self
}
pub fn ignore_content_type(mut self, ignore: bool) -> Self {
self.ignore_content_type = ignore;
self
}
pub fn ignore_properties(mut self, ignore: bool) -> Self {
self.ignore_properties = ignore;
self
}
pub fn properties_only(mut self, only: bool) -> Self {
self.properties_only = only;
self
}
pub fn use_git_diff_format(mut self, use_git: bool) -> Self {
self.use_git_diff_format = use_git;
self
}
pub fn header_encoding(mut self, encoding: impl Into<String>) -> Self {
self.header_encoding = encoding.into();
self
}
pub fn changelists(mut self, lists: Vec<String>) -> Self {
self.changelists = Some(lists);
self
}
pub fn execute(
self,
outstream: &mut crate::io::Stream,
errstream: &mut crate::io::Stream,
) -> Result<(), Error<'static>> {
let mut options = DiffOptions::new()
.with_diff_options(self.diff_options)
.with_depth(self.depth)
.with_ignore_ancestry(self.ignore_ancestry)
.with_no_diff_added(self.no_diff_added)
.with_no_diff_deleted(self.no_diff_deleted)
.with_show_copies_as_adds(self.show_copies_as_adds)
.with_ignore_content_type(self.ignore_content_type)
.with_ignore_properties(self.ignore_properties)
.with_properties_only(self.properties_only)
.with_use_git_diff_format(self.use_git_diff_format)
.with_header_encoding(self.header_encoding);
if let Some(cl) = self.changelists {
options = options.with_changelists(cl);
}
self.ctx.diff(
&self.path1,
&self.revision1,
&self.path2,
&self.revision2,
self.relative_to_dir.as_deref(),
outstream,
errstream,
&options,
)
}
}
pub struct ListBuilder<'a> {
ctx: &'a mut Context,
path_or_url: String,
peg_revision: Revision,
revision: Revision,
patterns: Option<Vec<String>>,
depth: Depth,
dirent_fields: u32,
fetch_locks: bool,
include_externals: bool,
}
impl<'a> ListBuilder<'a> {
pub fn new(ctx: &'a mut Context, path_or_url: impl Into<String>) -> Self {
Self {
ctx,
path_or_url: path_or_url.into(),
peg_revision: Revision::Head,
revision: Revision::Head,
patterns: None,
depth: Depth::Infinity,
dirent_fields: subversion_sys::SVN_DIRENT_KIND
| subversion_sys::SVN_DIRENT_SIZE
| subversion_sys::SVN_DIRENT_HAS_PROPS
| subversion_sys::SVN_DIRENT_CREATED_REV
| subversion_sys::SVN_DIRENT_TIME
| subversion_sys::SVN_DIRENT_LAST_AUTHOR,
fetch_locks: false,
include_externals: false,
}
}
pub fn peg_revision(mut self, rev: Revision) -> Self {
self.peg_revision = rev;
self
}
pub fn revision(mut self, rev: Revision) -> Self {
self.revision = rev;
self
}
pub fn patterns(mut self, patterns: Vec<String>) -> Self {
self.patterns = Some(patterns);
self
}
pub fn depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn dirent_fields(mut self, fields: u32) -> Self {
self.dirent_fields = fields;
self
}
pub fn fetch_locks(mut self, fetch: bool) -> Self {
self.fetch_locks = fetch;
self
}
pub fn include_externals(mut self, include: bool) -> Self {
self.include_externals = include;
self
}
pub fn execute(
self,
list_func: &mut dyn FnMut(
&str,
&crate::DirEntry,
Option<&crate::Lock>,
) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let options = ListOptions {
peg_revision: self.peg_revision,
revision: self.revision,
patterns: self.patterns,
depth: self.depth,
dirent_fields: self.dirent_fields,
fetch_locks: self.fetch_locks,
include_externals: self.include_externals,
};
self.ctx.list(&self.path_or_url, &options, list_func)
}
}
pub struct CopyBuilder<'a> {
ctx: &'a mut Context,
sources: Vec<(String, Option<Revision>)>,
dst_path: String,
copy_as_child: bool,
make_parents: bool,
ignore_externals: bool,
metadata_only: bool,
pin_externals: bool,
}
impl<'a> CopyBuilder<'a> {
pub fn new(ctx: &'a mut Context, dst_path: impl Into<String>) -> Self {
Self {
ctx,
sources: Vec::new(),
dst_path: dst_path.into(),
copy_as_child: false,
make_parents: false,
ignore_externals: false,
metadata_only: false,
pin_externals: false,
}
}
pub fn add_source(mut self, path: impl Into<String>, revision: Option<Revision>) -> Self {
self.sources.push((path.into(), revision));
self
}
pub fn copy_as_child(mut self, as_child: bool) -> Self {
self.copy_as_child = as_child;
self
}
pub fn make_parents(mut self, make: bool) -> Self {
self.make_parents = make;
self
}
pub fn ignore_externals(mut self, ignore: bool) -> Self {
self.ignore_externals = ignore;
self
}
pub fn metadata_only(mut self, only: bool) -> Self {
self.metadata_only = only;
self
}
pub fn pin_externals(mut self, pin: bool) -> Self {
self.pin_externals = pin;
self
}
pub fn execute(self) -> Result<(), Error<'static>> {
let sources: Vec<(&str, Option<Revision>)> = self
.sources
.iter()
.map(|(path, rev)| (path.as_str(), *rev))
.collect();
let mut options = CopyOptions::new()
.with_copy_as_child(self.copy_as_child)
.with_make_parents(self.make_parents)
.with_ignore_externals(self.ignore_externals)
.with_metadata_only(self.metadata_only)
.with_pin_externals(self.pin_externals);
self.ctx.copy(&sources, &self.dst_path, &mut options)
}
}
pub struct InfoBuilder<'a> {
ctx: &'a mut Context,
abspath_or_url: String,
peg_revision: Revision,
revision: Revision,
depth: Depth,
fetch_excluded: bool,
fetch_actual_only: bool,
include_externals: bool,
changelists: Option<Vec<String>>,
}
impl<'a> InfoBuilder<'a> {
pub fn new(ctx: &'a mut Context, abspath_or_url: impl Into<String>) -> Self {
Self {
ctx,
abspath_or_url: abspath_or_url.into(),
peg_revision: Revision::Head,
revision: Revision::Head,
depth: Depth::Infinity,
fetch_excluded: false,
fetch_actual_only: false,
include_externals: false,
changelists: None,
}
}
pub fn peg_revision(mut self, rev: Revision) -> Self {
self.peg_revision = rev;
self
}
pub fn revision(mut self, rev: Revision) -> Self {
self.revision = rev;
self
}
pub fn depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn fetch_excluded(mut self, fetch: bool) -> Self {
self.fetch_excluded = fetch;
self
}
pub fn fetch_actual_only(mut self, fetch: bool) -> Self {
self.fetch_actual_only = fetch;
self
}
pub fn include_externals(mut self, include: bool) -> Self {
self.include_externals = include;
self
}
pub fn changelists(mut self, lists: Vec<String>) -> Self {
self.changelists = Some(lists);
self
}
pub fn execute(
self,
receiver: &dyn FnMut(&std::path::Path, &Info) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let options = InfoOptions {
peg_revision: self.peg_revision,
revision: self.revision,
depth: self.depth,
fetch_excluded: self.fetch_excluded,
fetch_actual_only: self.fetch_actual_only,
include_externals: self.include_externals,
changelists: self.changelists,
};
self.ctx.info(&self.abspath_or_url, &options, receiver)
}
}
pub struct CommitBuilder<'a> {
ctx: &'a mut Context,
targets: Vec<String>,
depth: Depth,
keep_locks: bool,
keep_changelists: bool,
commit_as_operations: bool,
include_file_externals: bool,
include_dir_externals: bool,
changelists: Option<Vec<String>>,
revprop_table: std::collections::HashMap<String, String>,
}
impl<'a> CommitBuilder<'a> {
pub fn new(ctx: &'a mut Context) -> Self {
Self {
ctx,
targets: Vec::new(),
depth: Depth::Infinity,
keep_locks: false,
keep_changelists: false,
commit_as_operations: true,
include_file_externals: false,
include_dir_externals: false,
changelists: None,
revprop_table: std::collections::HashMap::new(),
}
}
pub fn add_target(mut self, target: impl Into<String>) -> Self {
self.targets.push(target.into());
self
}
pub fn targets(mut self, targets: Vec<String>) -> Self {
self.targets = targets;
self
}
pub fn depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn keep_locks(mut self, keep: bool) -> Self {
self.keep_locks = keep;
self
}
pub fn keep_changelists(mut self, keep: bool) -> Self {
self.keep_changelists = keep;
self
}
pub fn commit_as_operations(mut self, as_ops: bool) -> Self {
self.commit_as_operations = as_ops;
self
}
pub fn include_file_externals(mut self, include: bool) -> Self {
self.include_file_externals = include;
self
}
pub fn include_dir_externals(mut self, include: bool) -> Self {
self.include_dir_externals = include;
self
}
pub fn changelists(mut self, lists: Vec<String>) -> Self {
self.changelists = Some(lists);
self
}
pub fn add_revprop(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.revprop_table.insert(name.into(), value.into());
self
}
pub fn revprops(mut self, props: std::collections::HashMap<String, String>) -> Self {
self.revprop_table = props;
self
}
pub fn execute(
self,
log_msg_func: Option<&mut dyn FnMut(&[CommitItem]) -> Result<String, Error<'static>>>,
commit_callback: &mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let targets_ref: Vec<&str> = self.targets.iter().map(|s| s.as_str()).collect();
let revprop_ref: std::collections::HashMap<&str, &str> = self
.revprop_table
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
let changelists_vec = self
.changelists
.as_ref()
.map(|cl| cl.iter().map(|s| s.to_string()).collect::<Vec<_>>());
let options = CommitOptions {
depth: self.depth,
keep_locks: self.keep_locks,
keep_changelists: self.keep_changelists,
commit_as_operations: self.commit_as_operations,
include_file_externals: self.include_file_externals,
include_dir_externals: self.include_dir_externals,
changelists: changelists_vec,
};
self.ctx.commit(
&targets_ref,
&options,
revprop_ref,
log_msg_func,
commit_callback,
)
}
}
pub struct LogBuilder<'a> {
ctx: &'a mut Context,
targets: Vec<String>,
peg_revision: Revision,
revision_ranges: Vec<RevisionRange>,
limit: i32,
discover_changed_paths: bool,
strict_node_history: bool,
include_merged_revisions: bool,
revprops: Option<Vec<String>>,
}
impl<'a> LogBuilder<'a> {
pub fn new(ctx: &'a mut Context) -> Self {
Self {
ctx,
targets: Vec::new(),
peg_revision: Revision::Head,
revision_ranges: vec![RevisionRange::new(
Revision::Number(Revnum(1)),
Revision::Head,
)],
limit: 0, discover_changed_paths: false,
strict_node_history: true,
include_merged_revisions: false,
revprops: None,
}
}
pub fn add_target(mut self, target: impl Into<String>) -> Self {
self.targets.push(target.into());
self
}
pub fn targets(mut self, targets: Vec<String>) -> Self {
self.targets = targets;
self
}
pub fn peg_revision(mut self, rev: Revision) -> Self {
self.peg_revision = rev;
self
}
pub fn add_revision_range(mut self, start: Revision, end: Revision) -> Self {
self.revision_ranges.push(RevisionRange::new(start, end));
self
}
pub fn revision_ranges(mut self, ranges: Vec<RevisionRange>) -> Self {
self.revision_ranges = ranges;
self
}
pub fn limit(mut self, limit: i32) -> Self {
self.limit = limit;
self
}
pub fn discover_changed_paths(mut self, discover: bool) -> Self {
self.discover_changed_paths = discover;
self
}
pub fn strict_node_history(mut self, strict: bool) -> Self {
self.strict_node_history = strict;
self
}
pub fn include_merged_revisions(mut self, include: bool) -> Self {
self.include_merged_revisions = include;
self
}
pub fn add_revprop(mut self, prop: impl Into<String>) -> Self {
if let Some(ref mut revprops) = self.revprops {
revprops.push(prop.into());
} else {
self.revprops = Some(vec![prop.into()]);
}
self
}
pub fn revprops(mut self, props: Option<Vec<String>>) -> Self {
self.revprops = props;
self
}
pub fn execute(
self,
log_entry_receiver: &dyn FnMut(&LogEntry) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let targets_ref: Vec<&str> = self.targets.iter().map(|s| s.as_str()).collect();
let mut options = LogOptions::new()
.with_peg_revision(self.peg_revision)
.with_discover_changed_paths(self.discover_changed_paths)
.with_strict_node_history(self.strict_node_history)
.with_include_merged_revisions(self.include_merged_revisions)
.with_revprops(self.revprops);
if self.limit != 0 {
options = options.with_limit(self.limit);
}
self.ctx.log(
&targets_ref,
&self.revision_ranges,
&options,
log_entry_receiver,
)
}
}
pub struct UpdateBuilder<'a> {
ctx: &'a mut Context,
paths: Vec<String>,
revision: Revision,
depth: Depth,
depth_is_sticky: bool,
ignore_externals: bool,
allow_unver_obstructions: bool,
adds_as_modifications: bool,
make_parents: bool,
}
impl<'a> UpdateBuilder<'a> {
pub fn new(ctx: &'a mut Context) -> Self {
Self {
ctx,
paths: Vec::new(),
revision: Revision::Head,
depth: Depth::Infinity,
depth_is_sticky: false,
ignore_externals: false,
allow_unver_obstructions: false,
adds_as_modifications: false,
make_parents: false,
}
}
pub fn add_path(mut self, path: impl Into<String>) -> Self {
self.paths.push(path.into());
self
}
pub fn paths(mut self, paths: Vec<String>) -> Self {
self.paths = paths;
self
}
pub fn revision(mut self, rev: Revision) -> Self {
self.revision = rev;
self
}
pub fn depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
pub fn depth_is_sticky(mut self, sticky: bool) -> Self {
self.depth_is_sticky = sticky;
self
}
pub fn ignore_externals(mut self, ignore: bool) -> Self {
self.ignore_externals = ignore;
self
}
pub fn allow_unver_obstructions(mut self, allow: bool) -> Self {
self.allow_unver_obstructions = allow;
self
}
pub fn adds_as_modification(mut self, as_mod: bool) -> Self {
self.adds_as_modifications = as_mod;
self
}
pub fn make_parents(mut self, make: bool) -> Self {
self.make_parents = make;
self
}
pub fn execute(self) -> Result<Vec<Revnum>, Error<'static>> {
let paths_ref: Vec<&str> = self.paths.iter().map(|s| s.as_str()).collect();
let options = UpdateOptions {
depth: self.depth,
depth_is_sticky: self.depth_is_sticky,
ignore_externals: self.ignore_externals,
allow_unver_obstructions: self.allow_unver_obstructions,
adds_as_modifications: self.adds_as_modifications,
make_parents: self.make_parents,
};
self.ctx.update(&paths_ref, self.revision, &options)
}
}
pub struct MkdirBuilder<'a> {
ctx: &'a mut Context,
paths: Vec<String>,
make_parents: bool,
revprop_table: std::collections::HashMap<String, Vec<u8>>,
}
impl<'a> MkdirBuilder<'a> {
pub fn new(ctx: &'a mut Context) -> Self {
Self {
ctx,
paths: Vec::new(),
make_parents: false,
revprop_table: std::collections::HashMap::new(),
}
}
pub fn add_path(mut self, path: impl Into<String>) -> Self {
self.paths.push(path.into());
self
}
pub fn paths(mut self, paths: Vec<String>) -> Self {
self.paths = paths;
self
}
pub fn make_parents(mut self, make: bool) -> Self {
self.make_parents = make;
self
}
pub fn add_revprop(mut self, name: impl Into<String>, value: Vec<u8>) -> Self {
self.revprop_table.insert(name.into(), value);
self
}
pub fn revprops(mut self, props: std::collections::HashMap<String, Vec<u8>>) -> Self {
self.revprop_table = props;
self
}
pub fn execute(
self,
commit_callback: &mut dyn FnMut(&crate::CommitInfo) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let paths_ref: Vec<&str> = self.paths.iter().map(|s| s.as_str()).collect();
let revprop_table_owned: std::collections::HashMap<String, Vec<u8>> =
self.revprop_table.into_iter().collect();
let mut options = MkdirOptions::new()
.with_make_parents(self.make_parents)
.with_commit_callback(commit_callback);
if !revprop_table_owned.is_empty() {
options = options.with_revprop_table(revprop_table_owned);
}
self.ctx.mkdir(&paths_ref, &mut options)
}
}
impl Context {
pub fn diff_builder<'a>(
&'a mut self,
path1: impl Into<String>,
revision1: Revision,
path2: impl Into<String>,
revision2: Revision,
) -> DiffBuilder<'a> {
DiffBuilder::new(self, path1, revision1, path2, revision2)
}
pub fn list_builder<'a>(&'a mut self, path_or_url: impl Into<String>) -> ListBuilder<'a> {
ListBuilder::new(self, path_or_url)
}
pub fn copy_builder<'a>(&'a mut self, dst_path: impl Into<String>) -> CopyBuilder<'a> {
CopyBuilder::new(self, dst_path)
}
pub fn revert(
&mut self,
paths: &[&str],
options: &RevertOptions,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let mut paths_array = apr::tables::TypedArray::<*const i8>::new(&pool, paths.len() as i32);
let path_cstrings: Vec<_> = paths
.iter()
.map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
.collect();
for cstring in &path_cstrings {
paths_array.push(cstring.as_ptr());
}
let (changelists_array, list_cstrings) = if let Some(lists) = &options.changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, lists.len() as i32);
let cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(l.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(unsafe { array.as_ptr() }, Some(cstrings))
} else {
(std::ptr::null(), None)
};
let err = unsafe {
subversion_sys::svn_client_revert4(
paths_array.as_ptr(),
options.depth.into(),
changelists_array,
options.clear_changelists as i32,
options.metadata_only as i32,
options.added_keep_local as i32,
self.ptr,
pool.as_mut_ptr(),
)
};
drop(list_cstrings);
Error::from_raw(err)?;
Ok(())
}
pub fn resolved(
&mut self,
path: impl AsCanonicalDirent,
recursive: bool,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let path = crate::dirent::to_absolute_cstring(path)?;
let err = unsafe {
subversion_sys::svn_client_resolved(
path.as_ptr(),
recursive as i32,
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn add_to_changelist(
&mut self,
targets: &[&str],
changelist: &str,
depth: Depth,
changelists: Option<&[&str]>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let changelist = std::ffi::CString::new(changelist).unwrap();
let mut targets_array =
apr::tables::TypedArray::<*const i8>::new(&pool, targets.len() as i32);
let target_cstrings: Vec<_> = targets
.iter()
.map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
.collect();
for cstring in &target_cstrings {
targets_array.push(cstring.as_ptr());
}
let changelists_array = if let Some(lists) = changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, lists.len() as i32);
let list_cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(*l).unwrap())
.collect();
for cstring in &list_cstrings {
array.push(cstring.as_ptr());
}
unsafe { array.as_ptr() }
} else {
std::ptr::null()
};
let err = unsafe {
subversion_sys::svn_client_add_to_changelist(
targets_array.as_ptr(),
changelist.as_ptr(),
depth.into(),
changelists_array,
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn remove_from_changelists(
&mut self,
targets: &[&str],
depth: Depth,
changelists: Option<&[&str]>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let mut targets_array =
apr::tables::TypedArray::<*const i8>::new(&pool, targets.len() as i32);
let target_cstrings: Vec<_> = targets
.iter()
.map(|t| crate::dirent::canonicalize_path_or_url(t).unwrap())
.collect();
for cstring in &target_cstrings {
targets_array.push(cstring.as_ptr());
}
let changelists_array = if let Some(lists) = changelists {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, lists.len() as i32);
let list_cstrings: Vec<_> = lists
.iter()
.map(|l| std::ffi::CString::new(*l).unwrap())
.collect();
for cstring in &list_cstrings {
array.push(cstring.as_ptr());
}
unsafe { array.as_ptr() }
} else {
std::ptr::null()
};
let err = unsafe {
subversion_sys::svn_client_remove_from_changelists(
targets_array.as_ptr(),
depth.into(),
changelists_array,
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn get_changelists(
&mut self,
path: &str,
depth: Depth,
changelists: Option<&[&str]>,
receiver: &mut dyn FnMut(&str, &str) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let path_cstr = crate::dirent::canonicalize_path_or_url(path).unwrap();
let cstrings: Vec<_> = changelists
.map(|lists| {
lists
.iter()
.map(|l| std::ffi::CString::new(*l).unwrap())
.collect::<Vec<_>>()
})
.unwrap_or_default();
let changelists_array = if changelists.is_some() {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, cstrings.len() as i32);
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
unsafe { array.as_ptr() }
} else {
std::ptr::null()
};
let callback_baton = &receiver as *const _ as *mut std::ffi::c_void;
let err = unsafe {
subversion_sys::svn_client_get_changelists(
path_cstr.as_ptr(),
changelists_array,
depth.into(),
Some(wrap_changelist_receiver),
callback_baton,
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn merge_sources(
&mut self,
source1: &str,
revision1: &Revision,
source2: &str,
revision2: &Revision,
target_wcpath: &str,
depth: Depth,
options: &MergeSourcesOptions,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let source1 = crate::dirent::canonicalize_path_or_url(source1).unwrap();
let source2 = crate::dirent::canonicalize_path_or_url(source2).unwrap();
let target_wcpath = crate::dirent::to_absolute_cstring(target_wcpath)?;
let merge_opts_array = options.merge_options.as_ref().map(|opts| {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, opts.len() as i32);
let cstrings: Vec<_> = opts
.iter()
.map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(array, cstrings)
});
let merge_opts_ptr = merge_opts_array
.as_ref()
.map_or(std::ptr::null(), |(arr, _)| unsafe { arr.as_ptr() });
let err = unsafe {
subversion_sys::svn_client_merge5(
source1.as_ptr(),
&(*revision1).into(),
source2.as_ptr(),
&(*revision2).into(),
target_wcpath.as_ptr(),
depth.into(),
options.ignore_mergeinfo as i32,
options.diff_ignore_ancestry as i32,
options.force_delete as i32,
options.record_only as i32,
options.dry_run as i32,
options.allow_mixed_rev as i32,
merge_opts_ptr,
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn merge_peg(
&mut self,
source: &str,
ranges_to_merge: &[crate::mergeinfo::MergeRange],
peg_revision: &Revision,
target_wcpath: &str,
depth: Depth,
options: &MergeSourcesOptions,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let source = crate::dirent::canonicalize_path_or_url(source)?;
let target_wcpath = crate::dirent::to_absolute_cstring(target_wcpath)?;
let ranges_array = if ranges_to_merge.is_empty() {
std::ptr::null()
} else {
unsafe {
let mut array =
apr::tables::TypedArray::<*mut subversion_sys::svn_merge_range_t>::new(
&pool,
ranges_to_merge.len() as i32,
);
for range in ranges_to_merge {
let range_ptr: *mut subversion_sys::svn_merge_range_t = pool.calloc();
(*range_ptr).start = range.start.0;
(*range_ptr).end = range.end.0;
(*range_ptr).inheritable = range.inheritable as i32;
array.push(range_ptr);
}
array.as_ptr()
}
};
let merge_opts_array = options.merge_options.as_ref().map(|opts| {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, opts.len() as i32);
let cstrings: Vec<_> = opts
.iter()
.map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(array, cstrings)
});
let merge_opts_ptr = merge_opts_array
.as_ref()
.map_or(std::ptr::null(), |(arr, _)| unsafe { arr.as_ptr() });
let peg_rev_c: subversion_sys::svn_opt_revision_t = (*peg_revision).into();
let err = unsafe {
subversion_sys::svn_client_merge_peg5(
source.as_ptr(),
ranges_array,
&peg_rev_c,
target_wcpath.as_ptr(),
depth.into(),
options.ignore_mergeinfo as i32,
options.diff_ignore_ancestry as i32,
options.force_delete as i32,
options.record_only as i32,
options.dry_run as i32,
options.allow_mixed_rev as i32,
merge_opts_ptr,
self.ptr,
pool.as_mut_ptr(),
)
};
svn_result(err)
}
pub fn merge(
&mut self,
source1: &str,
revision1: &Revision,
source2: &str,
revision2: &Revision,
target_wcpath: &str,
depth: Depth,
options: &MergeSourcesOptions,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let source1 = crate::dirent::canonicalize_path_or_url(source1)?;
let source2 = crate::dirent::canonicalize_path_or_url(source2)?;
let target_wcpath = crate::dirent::to_absolute_cstring(target_wcpath)?;
let merge_opts_array = options.merge_options.as_ref().map(|opts| {
let mut array = apr::tables::TypedArray::<*const i8>::new(&pool, opts.len() as i32);
let cstrings: Vec<_> = opts
.iter()
.map(|opt| std::ffi::CString::new(opt.as_str()).unwrap())
.collect();
for cstring in &cstrings {
array.push(cstring.as_ptr());
}
(array, cstrings)
});
let merge_opts_ptr = merge_opts_array
.as_ref()
.map_or(std::ptr::null(), |(arr, _)| unsafe { arr.as_ptr() });
let rev1: subversion_sys::svn_opt_revision_t = (*revision1).into();
let rev2: subversion_sys::svn_opt_revision_t = (*revision2).into();
let err = unsafe {
subversion_sys::svn_client_merge5(
source1.as_ptr(),
&rev1,
source2.as_ptr(),
&rev2,
target_wcpath.as_ptr(),
depth.into(),
options.ignore_mergeinfo as i32,
options.diff_ignore_ancestry as i32,
options.force_delete as i32,
options.record_only as i32,
options.dry_run as i32,
options.allow_mixed_rev as i32,
merge_opts_ptr,
self.ptr,
pool.as_mut_ptr(),
)
};
svn_result(err)
}
pub fn get_merging_summary(
&mut self,
source_path_or_url: &str,
source_revision: &Revision,
target_path_or_url: &str,
target_revision: &Revision,
) -> Result<
(
bool,
String,
i64,
String,
i64,
String,
i64,
String,
i64,
String,
),
Error<'_>,
> {
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let source_c = crate::dirent::canonicalize_path_or_url(source_path_or_url)?;
let target_c = crate::dirent::canonicalize_path_or_url(target_path_or_url)?;
let source_rev: subversion_sys::svn_opt_revision_t = (*source_revision).into();
let target_rev: subversion_sys::svn_opt_revision_t = (*target_revision).into();
let mut needs_reintegration: i32 = 0;
let mut yca_url: *const i8 = std::ptr::null();
let mut yca_rev: subversion_sys::svn_revnum_t = 0;
let mut base_url: *const i8 = std::ptr::null();
let mut base_rev: subversion_sys::svn_revnum_t = 0;
let mut right_url: *const i8 = std::ptr::null();
let mut right_rev: subversion_sys::svn_revnum_t = 0;
let mut target_url: *const i8 = std::ptr::null();
let mut target_rev_out: subversion_sys::svn_revnum_t = 0;
let mut repos_root_url: *const i8 = std::ptr::null();
let err = unsafe {
subversion_sys::svn_client_get_merging_summary(
&mut needs_reintegration,
&mut yca_url,
&mut yca_rev,
&mut base_url,
&mut base_rev,
&mut right_url,
&mut right_rev,
&mut target_url,
&mut target_rev_out,
&mut repos_root_url,
source_c.as_ptr(),
&source_rev,
target_c.as_ptr(),
&target_rev,
self.ptr,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
unsafe {
let yca_url_str = if yca_url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(yca_url)
.to_string_lossy()
.into_owned()
};
let base_url_str = if base_url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(base_url)
.to_string_lossy()
.into_owned()
};
let right_url_str = if right_url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(right_url)
.to_string_lossy()
.into_owned()
};
let target_url_str = if target_url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(target_url)
.to_string_lossy()
.into_owned()
};
let repos_root_url_str = if repos_root_url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(repos_root_url)
.to_string_lossy()
.into_owned()
};
Ok((
needs_reintegration != 0,
yca_url_str,
yca_rev.into(),
base_url_str,
base_rev.into(),
right_url_str,
right_rev.into(),
target_url_str,
target_rev_out.into(),
repos_root_url_str,
))
}
}
pub fn move_path(
&mut self,
src_paths: &[&str],
dst_path: &str,
options: &mut MoveOptions,
) -> Result<(), Error<'static>> {
let pool = Pool::new();
let mut src_paths_array =
apr::tables::TypedArray::<*const i8>::new(&pool, src_paths.len() as i32);
let src_cstrings: Vec<_> = src_paths
.iter()
.map(|p| crate::dirent::canonicalize_path_or_url(p).unwrap())
.collect();
for cstring in &src_cstrings {
src_paths_array.push(cstring.as_ptr());
}
let dst_path = crate::dirent::canonicalize_path_or_url(dst_path)?;
let mut revprop_hash = std::ptr::null_mut();
if let Some(ref revprops) = options.revprop_table {
let mut hash = apr::hash::Hash::new(&pool);
for (key, value) in revprops {
let key_cstring = std::ffi::CString::new(key.as_str()).unwrap();
let svn_string = crate::string::BStr::from_bytes(value, &pool);
unsafe {
hash.insert(
key_cstring.as_bytes(),
svn_string.as_ptr() as *mut std::ffi::c_void,
);
}
}
unsafe {
revprop_hash = hash.as_mut_ptr();
}
}
let (callback_func, callback_baton) = if let Some(ref mut cb) = options.commit_callback {
(
Some(crate::wrap_commit_callback2 as _),
cb as *mut _ as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
let err = unsafe {
subversion_sys::svn_client_move7(
src_paths_array.as_ptr(),
dst_path.as_ptr(),
options.move_as_child as i32,
options.make_parents as i32,
options.allow_mixed_revisions as i32,
options.metadata_only as i32,
revprop_hash,
callback_func,
callback_baton,
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn info_builder<'a>(&'a mut self, abspath_or_url: impl Into<String>) -> InfoBuilder<'a> {
InfoBuilder::new(self, abspath_or_url)
}
pub fn commit_builder<'a>(&'a mut self) -> CommitBuilder<'a> {
CommitBuilder::new(self)
}
pub fn log_builder<'a>(&'a mut self) -> LogBuilder<'a> {
LogBuilder::new(self)
}
pub fn update_builder<'a>(&'a mut self) -> UpdateBuilder<'a> {
UpdateBuilder::new(self)
}
pub fn mkdir_builder<'a>(&'a mut self) -> MkdirBuilder<'a> {
MkdirBuilder::new(self)
}
pub fn patch(
&mut self,
patch_path: &std::path::Path,
wc_dir_path: &std::path::Path,
options: &mut PatchOptions,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let patch_path_cstr = crate::dirent::to_absolute_cstring(patch_path)?;
let wc_dir_path_cstr = crate::dirent::to_absolute_cstring(wc_dir_path)?;
extern "C" fn c_patch_callback(
baton: *mut std::ffi::c_void,
filtered: *mut i32,
canon_path: *const i8,
patch_abspath: *const i8,
reject_abspath: *const i8,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
unsafe {
let callback = &mut *(baton
as *mut &mut dyn FnMut(
&mut bool,
&str,
&std::path::Path,
&std::path::Path,
) -> Result<(), Error<'static>>);
let canon_path_str = std::ffi::CStr::from_ptr(canon_path).to_str().unwrap_or("");
let patch_path = std::path::Path::new(
std::ffi::CStr::from_ptr(patch_abspath)
.to_str()
.unwrap_or(""),
);
let reject_path = std::path::Path::new(
std::ffi::CStr::from_ptr(reject_abspath)
.to_str()
.unwrap_or(""),
);
let mut filtered_bool = *filtered != 0;
match callback(&mut filtered_bool, canon_path_str, patch_path, reject_path) {
Ok(()) => {
*filtered = filtered_bool as i32;
std::ptr::null_mut()
}
Err(err) => err.into_raw(),
}
}
}
let (callback_func, callback_baton) = if let Some(ref mut cb) = options.patch_func {
(
Some(c_patch_callback as _),
cb as *mut _ as *mut std::ffi::c_void,
)
} else {
(None, std::ptr::null_mut())
};
unsafe {
let err = subversion_sys::svn_client_patch(
patch_path_cstr.as_ptr(),
wc_dir_path_cstr.as_ptr(),
options.dry_run as i32,
options.strip_count,
options.reverse as i32,
options.ignore_whitespace as i32,
options.remove_tempfiles as i32,
callback_func,
callback_baton,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn merge_reintegrate(
&mut self,
source_url: &str,
source_peg_revision: &Revision,
target_wcpath: &std::path::Path,
dry_run: bool,
merge_options: &[&str],
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let source_url_cstr = std::ffi::CString::new(source_url)?;
let target_wcpath_cstr = crate::dirent::to_absolute_cstring(target_wcpath)?;
let merge_options_array = unsafe {
let array = apr_sys::apr_array_make(
pool.as_mut_ptr(),
merge_options.len() as i32,
std::mem::size_of::<*const std::os::raw::c_char>() as i32,
);
for option in merge_options {
let option_ptr = pool.pstrdup(option);
let slot = apr_sys::apr_array_push(array) as *mut *const std::os::raw::c_char;
*slot = option_ptr;
}
array
};
unsafe {
let err = subversion_sys::svn_client_merge_reintegrate(
source_url_cstr.as_ptr(),
&(*source_peg_revision).into(),
target_wcpath_cstr.as_ptr(),
dry_run as i32,
merge_options_array,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn uuid_from_url(&mut self, url: &str) -> Result<String, Error<'static>> {
let url_cstr = std::ffi::CString::new(url)?;
let pool = apr::Pool::new();
let mut uuid_ptr = std::ptr::null();
let ret = unsafe {
subversion_sys::svn_client_uuid_from_url(
&mut uuid_ptr,
url_cstr.as_ptr(),
self.ptr,
pool.as_mut_ptr(),
)
};
Error::from_raw(ret)?;
if uuid_ptr.is_null() {
return Err(Error::from_message("Failed to get repository UUID"));
}
let uuid_str = unsafe {
std::ffi::CStr::from_ptr(uuid_ptr)
.to_string_lossy()
.into_owned()
};
Ok(uuid_str)
}
pub fn uuid_from_path(&mut self, path: &std::path::Path) -> Result<String, Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let pool = apr::Pool::new();
let mut uuid_ptr = std::ptr::null();
let ret = unsafe {
subversion_sys::svn_client_uuid_from_path2(
&mut uuid_ptr,
path_cstr.as_ptr(),
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(), )
};
Error::from_raw(ret)?;
if uuid_ptr.is_null() {
return Err(Error::from_message("Failed to get repository UUID"));
}
let uuid_str = unsafe {
std::ffi::CStr::from_ptr(uuid_ptr)
.to_string_lossy()
.into_owned()
};
Ok(uuid_str)
}
pub fn relocate(
&mut self,
wcroot_path: &std::path::Path,
from_prefix: &str,
to_prefix: &str,
ignore_externals: bool,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let wcroot_path_cstr = crate::dirent::to_absolute_cstring(wcroot_path)?;
let from_prefix_cstr = std::ffi::CString::new(from_prefix)?;
let to_prefix_cstr = std::ffi::CString::new(to_prefix)?;
unsafe {
let err = subversion_sys::svn_client_relocate2(
wcroot_path_cstr.as_ptr(),
from_prefix_cstr.as_ptr(),
to_prefix_cstr.as_ptr(),
ignore_externals as i32,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn revprop_set(
&mut self,
propname: &str,
propval: Option<&[u8]>,
url: &str,
options: &RevpropSetOptions,
) -> Result<(), Error<'static>> {
if !crate::props::name_is_valid(propname) {
return Err(Error::from_message(&format!(
"Invalid property name: '{}'",
propname
)));
}
let pool = apr::Pool::new();
let propname_cstr = std::ffi::CString::new(propname)?;
let url_cstr = std::ffi::CString::new(url)?;
let propval_svn = if let Some(val) = propval {
unsafe {
let svn_str = apr_sys::apr_palloc(
pool.as_mut_ptr(),
std::mem::size_of::<subversion_sys::svn_string_t>(),
) as *mut subversion_sys::svn_string_t;
(*svn_str).data = val.as_ptr() as *const std::os::raw::c_char;
(*svn_str).len = val.len();
svn_str
}
} else {
std::ptr::null_mut()
};
let original_propval_svn = if let Some(ref val) = options.original_propval {
unsafe {
let svn_str = apr_sys::apr_palloc(
pool.as_mut_ptr(),
std::mem::size_of::<subversion_sys::svn_string_t>(),
) as *mut subversion_sys::svn_string_t;
(*svn_str).data = val.as_ptr() as *const std::os::raw::c_char;
(*svn_str).len = val.len();
svn_str
}
} else {
std::ptr::null_mut()
};
unsafe {
let err = subversion_sys::svn_client_revprop_set2(
propname_cstr.as_ptr(),
propval_svn,
original_propval_svn,
url_cstr.as_ptr(),
&options.revision.into(),
std::ptr::null_mut(), options.force as i32,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn revprop_get(
&mut self,
propname: &str,
url: &str,
revision: &Revision,
) -> Result<Option<Vec<u8>>, Error<'_>> {
if !crate::props::name_is_valid(propname) {
return Err(Error::from_message(&format!(
"Invalid property name: '{}'",
propname
)));
}
let pool = apr::Pool::new();
let propname_cstr = std::ffi::CString::new(propname)?;
let url_cstr = std::ffi::CString::new(url)?;
let mut propval: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
let mut actual_rev = 0;
unsafe {
let err = subversion_sys::svn_client_revprop_get(
propname_cstr.as_ptr(),
&mut propval,
url_cstr.as_ptr(),
&(*revision).into(),
&mut actual_rev,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
if propval.is_null() {
Ok(None)
} else {
let len = (*propval).len;
let data = (*propval).data as *const u8;
Ok(Some(std::slice::from_raw_parts(data, len).to_vec()))
}
}
}
pub fn revprop_list(
&mut self,
url: &str,
revision: &Revision,
) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
let pool = apr::Pool::new();
let url_cstr = std::ffi::CString::new(url)?;
let mut props_hash = std::ptr::null_mut();
let mut actual_rev = 0;
unsafe {
let err = subversion_sys::svn_client_revprop_list(
&mut props_hash,
url_cstr.as_ptr(),
&(*revision).into(),
&mut actual_rev,
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
let result = if !props_hash.is_null() {
let prop_hash = crate::props::PropHash::from_ptr(props_hash);
prop_hash.to_hashmap()
} else {
std::collections::HashMap::new()
};
Ok(result)
}
}
pub fn suggest_merge_sources(
&mut self,
path_or_url: &str,
peg_revision: &Revision,
) -> Result<Vec<String>, Error<'_>> {
let pool = apr::Pool::new();
let path_or_url_cstr = crate::dirent::canonicalize_path_or_url(path_or_url)?;
let mut sources_array = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_client_suggest_merge_sources(
&mut sources_array,
path_or_url_cstr.as_ptr(),
&(*peg_revision).into(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)?;
let mut result = Vec::new();
if !sources_array.is_null() {
let array_len = (*sources_array).nelts as usize;
let array_data = (*sources_array).elts as *const *const std::os::raw::c_char;
for i in 0..array_len {
let source_ptr = *array_data.add(i);
if !source_ptr.is_null() {
let source_str = std::ffi::CStr::from_ptr(source_ptr);
result.push(source_str.to_string_lossy().into_owned());
}
}
}
Ok(result)
}
}
pub fn upgrade(&mut self, wcroot_path: &std::path::Path) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let wcroot_path_cstr = crate::dirent::to_absolute_cstring(wcroot_path)?;
unsafe {
let err = subversion_sys::svn_client_upgrade(
wcroot_path_cstr.as_ptr(),
self.as_mut_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
}
pub struct Status {
ptr: *const subversion_sys::svn_client_status_t,
_pool: apr::PoolHandle<'static>,
}
impl Status {
pub fn kind(&self) -> crate::NodeKind {
unsafe { (*self.ptr).kind.into() }
}
pub fn local_abspath(&self) -> &str {
unsafe {
std::ffi::CStr::from_ptr((*self.ptr).local_abspath)
.to_str()
.unwrap()
}
}
pub fn local_abspath_native(&self) -> std::path::PathBuf {
unsafe {
let pool = apr::Pool::new();
let local = subversion_sys::svn_dirent_local_style(
(*self.ptr).local_abspath,
pool.as_mut_ptr(),
);
std::path::PathBuf::from(std::ffi::CStr::from_ptr(local).to_str().unwrap())
}
}
pub fn filesize(&self) -> i64 {
unsafe { (*self.ptr).filesize }
}
pub fn versioned(&self) -> bool {
unsafe { (*self.ptr).versioned != 0 }
}
pub fn conflicted(&self) -> bool {
unsafe { (*self.ptr).conflicted != 0 }
}
pub fn node_status(&self) -> crate::StatusKind {
unsafe { (*self.ptr).node_status.into() }
}
pub fn text_status(&self) -> crate::StatusKind {
unsafe { (*self.ptr).text_status.into() }
}
pub fn prop_status(&self) -> crate::StatusKind {
unsafe { (*self.ptr).prop_status.into() }
}
pub fn wc_is_locked(&self) -> bool {
unsafe { (*self.ptr).wc_is_locked != 0 }
}
pub fn copied(&self) -> bool {
unsafe { (*self.ptr).copied != 0 }
}
pub fn repos_root_url(&self) -> &str {
unsafe {
std::ffi::CStr::from_ptr((*self.ptr).repos_root_url)
.to_str()
.unwrap()
}
}
pub fn repos_uuid(&self) -> &str {
unsafe {
std::ffi::CStr::from_ptr((*self.ptr).repos_uuid)
.to_str()
.unwrap()
}
}
pub fn repos_relpath(&self) -> &str {
unsafe {
std::ffi::CStr::from_ptr((*self.ptr).repos_relpath)
.to_str()
.unwrap()
}
}
pub fn revision(&self) -> Revnum {
Revnum::from_raw(unsafe { (*self.ptr).revision }).unwrap()
}
pub fn changed_rev(&self) -> Revnum {
Revnum::from_raw(unsafe { (*self.ptr).changed_rev }).unwrap()
}
pub fn changed_date(&self) -> apr::time::Time {
unsafe { apr::time::Time::from((*self.ptr).changed_date) }
}
pub fn changed_author(&self) -> &str {
unsafe {
std::ffi::CStr::from_ptr((*self.ptr).changed_author)
.to_str()
.unwrap()
}
}
pub fn switched(&self) -> bool {
unsafe { (*self.ptr).switched != 0 }
}
pub fn file_external(&self) -> bool {
unsafe { (*self.ptr).file_external != 0 }
}
pub fn lock(&self) -> Option<crate::Lock<'_>> {
let lock_ptr = unsafe { (*self.ptr).lock };
if lock_ptr.is_null() {
None
} else {
let pool_handle =
unsafe { apr::PoolHandle::from_borrowed_raw(self._pool.as_mut_ptr()) };
Some(crate::Lock::from_raw(lock_ptr, pool_handle))
}
}
pub fn changelist(&self) -> Option<&str> {
unsafe {
if (*self.ptr).changelist.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).changelist)
.to_str()
.unwrap(),
)
}
}
}
pub fn depth(&self) -> crate::Depth {
unsafe { (*self.ptr).depth.into() }
}
pub fn ood_kind(&self) -> crate::NodeKind {
unsafe { (*self.ptr).ood_kind.into() }
}
pub fn repos_node_status(&self) -> crate::StatusKind {
unsafe { (*self.ptr).repos_node_status.into() }
}
pub fn repos_text_status(&self) -> crate::StatusKind {
unsafe { (*self.ptr).repos_text_status.into() }
}
pub fn repos_prop_status(&self) -> crate::StatusKind {
unsafe { (*self.ptr).repos_prop_status.into() }
}
pub fn repos_lock(&self) -> Option<crate::Lock<'_>> {
let lock_ptr = unsafe { (*self.ptr).repos_lock };
if lock_ptr.is_null() {
None
} else {
let pool_handle =
unsafe { apr::PoolHandle::from_borrowed_raw(self._pool.as_mut_ptr()) };
Some(crate::Lock::from_raw(lock_ptr, pool_handle))
}
}
pub fn ood_changed_rev(&self) -> Option<Revnum> {
Revnum::from_raw(unsafe { (*self.ptr).ood_changed_rev })
}
pub fn ood_changed_author(&self) -> Option<&str> {
unsafe {
if (*self.ptr).ood_changed_author.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).ood_changed_author)
.to_str()
.unwrap(),
)
}
}
}
pub fn ood_changed_date(&self) -> apr::time::Time {
unsafe { apr::time::Time::from((*self.ptr).ood_changed_date) }
}
pub fn moved_from_abspath(&self) -> Option<&str> {
unsafe {
if (*self.ptr).moved_from_abspath.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).moved_from_abspath)
.to_str()
.unwrap(),
)
}
}
}
pub fn moved_to_abspath(&self) -> Option<&str> {
unsafe {
if (*self.ptr).moved_to_abspath.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).moved_to_abspath)
.to_str()
.unwrap(),
)
}
}
}
}
pub struct Conflict {
ptr: *mut subversion_sys::svn_client_conflict_t,
pool: apr::Pool<'static>,
_phantom: std::marker::PhantomData<*mut ()>, }
impl Drop for Conflict {
fn drop(&mut self) {
}
}
impl Conflict {
pub fn pool(&self) -> &apr::Pool<'_> {
&self.pool
}
pub fn as_ptr(&self) -> *const subversion_sys::svn_client_conflict_t {
self.ptr
}
pub fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_client_conflict_t {
self.ptr
}
pub(crate) unsafe fn from_ptr_and_pool(
ptr: *mut subversion_sys::svn_client_conflict_t,
pool: apr::Pool<'static>,
) -> Self {
Self {
ptr,
pool,
_phantom: std::marker::PhantomData,
}
}
pub fn prop_get_description(&mut self) -> Result<String, Error<'static>> {
let pool = apr::pool::Pool::new();
let mut description: *const i8 = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_client_conflict_prop_get_description(
&mut description,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(unsafe { std::ffi::CStr::from_ptr(description) }
.to_str()
.unwrap()
.to_owned())
}
pub fn text_resolve_by_id(
&mut self,
choice: crate::TextConflictChoice,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_text_resolve_by_id(
self.ptr,
choice.into(),
ctx.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn prop_resolve_by_id(
&mut self,
propname: &str,
choice: crate::TextConflictChoice,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
let propname_c = std::ffi::CString::new(propname)?;
unsafe {
let err = subversion_sys::svn_client_conflict_prop_resolve_by_id(
self.ptr,
propname_c.as_ptr(),
choice.into(),
ctx.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn prop_resolve_with_option(
&mut self,
propname: &str,
option: &ConflictOption,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
let propname_c = std::ffi::CString::new(propname)?;
unsafe {
let err = subversion_sys::svn_client_conflict_prop_resolve(
self.ptr,
propname_c.as_ptr(),
option.ptr,
ctx.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn text_resolve(
&mut self,
option: &ConflictOption,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_text_resolve(
self.ptr,
option.ptr,
ctx.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn tree_resolve_by_id(
&mut self,
choice: crate::TreeConflictChoice,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_tree_resolve_by_id(
self.ptr,
choice.into(),
ctx.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn tree_resolve(
&mut self,
option: &ConflictOption,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let scratch_pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_tree_resolve(
self.ptr,
option.ptr,
ctx.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn get_conflicted(&self) -> Result<(bool, Vec<String>, bool), Error<'_>> {
let pool = apr::pool::Pool::new();
let mut text_conflicted: subversion_sys::svn_boolean_t = 0;
let mut props_conflicted: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
let mut tree_conflicted: subversion_sys::svn_boolean_t = 0;
unsafe {
let err = subversion_sys::svn_client_conflict_get_conflicted(
&mut text_conflicted,
&mut props_conflicted,
&mut tree_conflicted,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let has_text_conflict = text_conflicted != 0;
let has_tree_conflict = tree_conflicted != 0;
let mut prop_conflicts = Vec::new();
if !props_conflicted.is_null() {
let array =
apr::tables::TypedArray::<*const std::ffi::c_char>::from_ptr(props_conflicted);
for cstr_ptr in array.iter() {
let cstr = std::ffi::CStr::from_ptr(cstr_ptr);
let propname = cstr.to_string_lossy().into_owned();
prop_conflicts.push(propname);
}
}
Ok((has_text_conflict, prop_conflicts, has_tree_conflict))
}
}
pub fn get_operation(&self) -> Result<crate::conflict::ConflictAction, Error<'static>> {
unsafe {
let operation = subversion_sys::svn_client_conflict_get_operation(self.ptr);
Ok(operation.into())
}
}
pub fn text_get_mime_type(&self) -> Result<Option<String>, Error<'_>> {
unsafe {
let mime_type = subversion_sys::svn_client_conflict_text_get_mime_type(self.ptr);
if mime_type.is_null() {
Ok(None)
} else {
Ok(Some(
std::ffi::CStr::from_ptr(mime_type)
.to_string_lossy()
.into_owned(),
))
}
}
}
pub fn get_local_abspath(&self) -> &str {
unsafe {
let abspath = subversion_sys::svn_client_conflict_get_local_abspath(self.ptr);
std::ffi::CStr::from_ptr(abspath).to_str().unwrap()
}
}
pub fn get_incoming_change(&self) -> crate::conflict::ConflictAction {
unsafe { subversion_sys::svn_client_conflict_get_incoming_change(self.ptr).into() }
}
pub fn get_local_change(&self) -> crate::conflict::ConflictAction {
unsafe { subversion_sys::svn_client_conflict_get_local_change(self.ptr).into() }
}
pub fn get_repos_info(&self) -> Result<(Option<String>, Option<String>), Error<'_>> {
let pool = apr::pool::Pool::new();
let mut repos_root_url: *const i8 = std::ptr::null();
let mut repos_uuid: *const i8 = std::ptr::null();
unsafe {
let err = subversion_sys::svn_client_conflict_get_repos_info(
&mut repos_root_url,
&mut repos_uuid,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let root_url = if repos_root_url.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(repos_root_url)
.to_str()
.unwrap()
.to_owned(),
)
};
let uuid = if repos_uuid.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(repos_uuid)
.to_str()
.unwrap()
.to_owned(),
)
};
Ok((root_url, uuid))
}
}
pub fn get_recommended_option_id(&self) -> crate::ClientConflictOptionId {
unsafe { subversion_sys::svn_client_conflict_get_recommended_option_id(self.ptr).into() }
}
pub fn text_get_resolution_options(
&self,
ctx: &mut Context,
) -> Result<Vec<ConflictOption>, Error<'_>> {
let pool = apr::pool::Pool::new();
let mut options: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_client_conflict_text_get_resolution_options(
&mut options,
self.ptr,
ctx.as_mut_ptr(),
self.pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let mut result = Vec::new();
if !options.is_null() {
let array = apr::tables::TypedArray::<
*mut subversion_sys::svn_client_conflict_option_t,
>::from_ptr(options);
for option_ptr in array.iter() {
result.push(ConflictOption {
ptr: option_ptr,
merged_value_pool: None,
});
}
}
Ok(result)
}
}
pub fn prop_get_resolution_options(
&self,
ctx: &mut Context,
) -> Result<Vec<ConflictOption>, Error<'_>> {
let pool = apr::pool::Pool::new();
let mut options: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_client_conflict_prop_get_resolution_options(
&mut options,
self.ptr,
ctx.as_mut_ptr(),
self.pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let mut result = Vec::new();
if !options.is_null() {
let array = apr::tables::TypedArray::<
*mut subversion_sys::svn_client_conflict_option_t,
>::from_ptr(options);
for option_ptr in array.iter() {
result.push(ConflictOption {
ptr: option_ptr,
merged_value_pool: None,
});
}
}
Ok(result)
}
}
pub fn tree_get_resolution_options(
&self,
ctx: &mut Context,
) -> Result<Vec<ConflictOption>, Error<'_>> {
let pool = apr::pool::Pool::new();
let mut options: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_client_conflict_tree_get_resolution_options(
&mut options,
self.ptr,
ctx.as_mut_ptr(),
self.pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let mut result = Vec::new();
if !options.is_null() {
let array = apr::tables::TypedArray::<
*mut subversion_sys::svn_client_conflict_option_t,
>::from_ptr(options);
for option_ptr in array.iter() {
result.push(ConflictOption {
ptr: option_ptr,
merged_value_pool: None,
});
}
}
Ok(result)
}
}
pub fn tree_get_description(
&self,
ctx: &mut Context,
) -> Result<(String, String), Error<'static>> {
let pool = apr::pool::Pool::new();
let mut incoming_desc: *const i8 = std::ptr::null();
let mut local_desc: *const i8 = std::ptr::null();
unsafe {
let err = subversion_sys::svn_client_conflict_tree_get_description(
&mut incoming_desc,
&mut local_desc,
self.ptr,
ctx.as_mut_ptr(),
self.pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let incoming = std::ffi::CStr::from_ptr(incoming_desc)
.to_str()
.unwrap()
.to_owned();
let local = std::ffi::CStr::from_ptr(local_desc)
.to_str()
.unwrap()
.to_owned();
Ok((incoming, local))
}
}
pub fn tree_get_details(&mut self, ctx: &mut Context) -> Result<(), Error<'static>> {
let pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_tree_get_details(
self.ptr,
ctx.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn tree_get_victim_node_kind(&self) -> crate::NodeKind {
unsafe { subversion_sys::svn_client_conflict_tree_get_victim_node_kind(self.ptr).into() }
}
pub fn get_incoming_old_repos_location(
&self,
) -> Result<(Option<String>, crate::Revnum, crate::NodeKind), Error<'_>> {
let pool = apr::pool::Pool::new();
let mut repos_relpath: *const i8 = std::ptr::null();
let mut revision: subversion_sys::svn_revnum_t = -1;
let mut node_kind: subversion_sys::svn_node_kind_t = 0;
unsafe {
let err = subversion_sys::svn_client_conflict_get_incoming_old_repos_location(
&mut repos_relpath,
&mut revision,
&mut node_kind,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let path = if repos_relpath.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(repos_relpath)
.to_str()
.unwrap()
.to_owned(),
)
};
Ok((path, crate::Revnum(revision), node_kind.into()))
}
}
pub fn get_incoming_new_repos_location(
&self,
) -> Result<(Option<String>, crate::Revnum, crate::NodeKind), Error<'_>> {
let pool = apr::pool::Pool::new();
let mut repos_relpath: *const i8 = std::ptr::null();
let mut revision: subversion_sys::svn_revnum_t = -1;
let mut node_kind: subversion_sys::svn_node_kind_t = 0;
unsafe {
let err = subversion_sys::svn_client_conflict_get_incoming_new_repos_location(
&mut repos_relpath,
&mut revision,
&mut node_kind,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let path = if repos_relpath.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(repos_relpath)
.to_str()
.unwrap()
.to_owned(),
)
};
Ok((path, crate::Revnum(revision), node_kind.into()))
}
}
pub fn text_get_resolution(&self) -> crate::ClientConflictOptionId {
unsafe { subversion_sys::svn_client_conflict_text_get_resolution(self.ptr).into() }
}
pub fn prop_get_resolution(
&self,
propname: &str,
) -> Result<crate::ClientConflictOptionId, Error<'static>> {
let propname_c = std::ffi::CString::new(propname)?;
unsafe {
Ok(subversion_sys::svn_client_conflict_prop_get_resolution(
self.ptr,
propname_c.as_ptr(),
)
.into())
}
}
pub fn tree_get_resolution(&self) -> crate::ClientConflictOptionId {
unsafe { subversion_sys::svn_client_conflict_tree_get_resolution(self.ptr).into() }
}
pub fn prop_get_reject_abspath(&self) -> Option<String> {
unsafe {
let path = subversion_sys::svn_client_conflict_prop_get_reject_abspath(self.ptr);
if path.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(path).to_str().unwrap().to_owned())
}
}
}
pub fn text_get_contents(
&self,
) -> Result<
(
Option<String>,
Option<String>,
Option<String>,
Option<String>,
),
Error<'_>,
> {
let pool = apr::pool::Pool::new();
let mut base: *const i8 = std::ptr::null();
let mut working: *const i8 = std::ptr::null();
let mut incoming_old: *const i8 = std::ptr::null();
let mut incoming_new: *const i8 = std::ptr::null();
unsafe {
let err = subversion_sys::svn_client_conflict_text_get_contents(
&mut base,
&mut working,
&mut incoming_old,
&mut incoming_new,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let to_opt_string = |ptr: *const i8| {
if ptr.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(ptr).to_str().unwrap().to_owned())
}
};
Ok((
to_opt_string(base),
to_opt_string(working),
to_opt_string(incoming_old),
to_opt_string(incoming_new),
))
}
}
pub fn prop_get_propvals(
&self,
propname: &str,
) -> Result<
(
Option<Vec<u8>>,
Option<Vec<u8>>,
Option<Vec<u8>>,
Option<Vec<u8>>,
),
Error<'_>,
> {
let pool = apr::pool::Pool::new();
let propname_c = std::ffi::CString::new(propname)?;
let mut base: *const subversion_sys::svn_string_t = std::ptr::null();
let mut working: *const subversion_sys::svn_string_t = std::ptr::null();
let mut incoming_old: *const subversion_sys::svn_string_t = std::ptr::null();
let mut incoming_new: *const subversion_sys::svn_string_t = std::ptr::null();
unsafe {
let err = subversion_sys::svn_client_conflict_prop_get_propvals(
&mut base,
&mut working,
&mut incoming_old,
&mut incoming_new,
self.ptr,
propname_c.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let to_opt_vec = |ptr: *const subversion_sys::svn_string_t| {
if ptr.is_null() {
None
} else {
let s = &*ptr;
if s.data.is_null() {
None
} else {
Some(std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec())
}
}
};
Ok((
to_opt_vec(base),
to_opt_vec(working),
to_opt_vec(incoming_old),
to_opt_vec(incoming_new),
))
}
}
}
pub struct ConflictOption {
ptr: *mut subversion_sys::svn_client_conflict_option_t,
merged_value_pool: Option<apr::Pool<'static>>,
}
impl ConflictOption {
pub fn get_id(&self) -> crate::ClientConflictOptionId {
unsafe { subversion_sys::svn_client_conflict_option_get_id(self.ptr).into() }
}
pub fn get_label(&self) -> String {
let pool = apr::pool::Pool::new();
unsafe {
let label =
subversion_sys::svn_client_conflict_option_get_label(self.ptr, pool.as_mut_ptr());
std::ffi::CStr::from_ptr(label).to_str().unwrap().to_owned()
}
}
pub fn get_description(&self) -> String {
let pool = apr::pool::Pool::new();
unsafe {
let desc = subversion_sys::svn_client_conflict_option_get_description(
self.ptr,
pool.as_mut_ptr(),
);
std::ffi::CStr::from_ptr(desc).to_str().unwrap().to_owned()
}
}
pub fn set_merged_propval(&mut self, merged_propval: Option<&[u8]>) {
let pool = apr::pool::Pool::new();
unsafe {
let svn_string = merged_propval.map(|val| {
subversion_sys::svn_string_ncreate(
val.as_ptr() as *const i8,
val.len(),
pool.as_mut_ptr(),
)
});
subversion_sys::svn_client_conflict_option_set_merged_propval(
self.ptr,
svn_string.unwrap_or(std::ptr::null_mut()),
);
}
self.merged_value_pool = Some(pool);
}
pub fn get_moved_to_repos_relpath_candidates(&self) -> Result<Vec<String>, Error<'_>> {
let pool = apr::pool::Pool::new();
let mut candidates: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
unsafe {
let err =
subversion_sys::svn_client_conflict_option_get_moved_to_repos_relpath_candidates2(
&mut candidates,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let mut result = Vec::new();
if !candidates.is_null() {
let array =
apr::tables::TypedArray::<*const std::ffi::c_char>::from_ptr(candidates);
for cstr_ptr in array.iter() {
let cstr = std::ffi::CStr::from_ptr(cstr_ptr);
result.push(cstr.to_string_lossy().into_owned());
}
}
Ok(result)
}
}
pub fn get_moved_to_abspath_candidates(&self) -> Result<Vec<std::path::PathBuf>, Error<'_>> {
let pool = apr::pool::Pool::new();
let mut candidates: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
unsafe {
let err = subversion_sys::svn_client_conflict_option_get_moved_to_abspath_candidates2(
&mut candidates,
self.ptr,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
let mut result = Vec::new();
if !candidates.is_null() {
let array =
apr::tables::TypedArray::<*const std::ffi::c_char>::from_ptr(candidates);
for cstr_ptr in array.iter() {
let cstr = std::ffi::CStr::from_ptr(cstr_ptr);
result.push(std::path::PathBuf::from(cstr.to_str()?));
}
}
Ok(result)
}
}
pub fn set_moved_to_repos_relpath(
&mut self,
preferred_move_target_idx: i32,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_option_set_moved_to_repos_relpath2(
self.ptr,
preferred_move_target_idx,
ctx.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn set_moved_to_abspath(
&mut self,
preferred_move_target_idx: i32,
ctx: &mut Context,
) -> Result<(), Error<'static>> {
let pool = apr::pool::Pool::new();
unsafe {
let err = subversion_sys::svn_client_conflict_option_set_moved_to_abspath2(
self.ptr,
preferred_move_target_idx,
ctx.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn find_by_id(
options: &[ConflictOption],
option_id: crate::ClientConflictOptionId,
) -> Option<&ConflictOption> {
options.iter().find(|opt| opt.get_id() == option_id)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
struct ClientTestFixture {
pub wc_path: PathBuf,
pub url: String,
pub ctx: Context,
pub temp_dir: tempfile::TempDir,
}
impl ClientTestFixture {
fn new() -> Self {
let temp_dir = tempfile::TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repo");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let repos_dirent = crate::dirent::Dirent::new(&repos_path).unwrap();
let uri = repos_dirent.to_file_url().unwrap();
let url = uri.to_string();
let mut ctx = Context::new().unwrap();
ctx.checkout(uri, &wc_path, &Self::default_checkout_options())
.unwrap();
Self {
wc_path,
url,
ctx,
temp_dir,
}
}
fn default_checkout_options() -> CheckoutOptions {
CheckoutOptions {
peg_revision: Revision::Head,
revision: Revision::Head,
depth: Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
}
}
fn add_file(&mut self, name: &str, content: &str) -> PathBuf {
let file_path = self.wc_path.join(name);
std::fs::write(&file_path, content).unwrap();
self.ctx.add(&file_path, &AddOptions::new()).unwrap();
file_path
}
fn add_dir(&mut self, name: &str) -> PathBuf {
let dir_path = self.wc_path.join(name);
std::fs::create_dir(&dir_path).unwrap();
self.ctx.add(&dir_path, &AddOptions::new()).unwrap();
dir_path
}
fn commit(&mut self) -> Revnum {
let commit_opts = CommitOptions::default();
let revprops = std::collections::HashMap::new();
let mut committed_rev = None;
self.ctx
.commit(
&[self
.wc_path
.to_str()
.expect("wc path should be valid UTF-8")],
&commit_opts,
revprops,
None,
&mut |info| {
committed_rev = Some(info.revision());
Ok(())
},
)
.unwrap();
committed_rev.unwrap_or(Revnum(0))
}
fn wc_path_str(&self) -> &str {
self.wc_path
.to_str()
.expect("working copy path should be valid UTF-8")
}
}
#[test]
fn test_version() {
let version = version();
assert_eq!(version.major(), 1);
}
#[test]
fn test_set_auth_unchecked() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let _repos = crate::repos::Repos::create(&repo_path).unwrap();
let dirent = crate::dirent::Dirent::new(&repo_path).unwrap();
let url = dirent.to_file_url().unwrap();
let providers = vec![crate::auth::get_username_provider()];
let mut auth_baton = crate::auth::AuthBaton::open(providers).unwrap();
let mut ctx = Context::new().unwrap();
unsafe { ctx.set_auth_unchecked(&mut auth_baton) };
let mut found = false;
ctx.info(
url.as_str(),
&InfoOptions {
revision: Revision::Head,
..Default::default()
},
&|_path, _info| {
found = true;
Ok(())
},
)
.unwrap();
assert!(found);
}
#[test]
fn test_open() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let mut repos = crate::repos::Repos::create(&repo_path).unwrap();
assert_eq!(repos.path(), td.path().join("repo"));
let mut ctx = Context::new().unwrap();
let dirent = crate::dirent::Dirent::new(&repo_path).unwrap();
let url = dirent.to_file_url().unwrap();
let revnum = ctx
.checkout(
url,
td.path().join("wc"),
&CheckoutOptions {
peg_revision: Revision::Head,
revision: Revision::Head,
depth: Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
assert_eq!(revnum, Revnum(0));
}
#[test]
fn test_copy() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "test content");
let copy_result = fixture.ctx.copy(
&[(
test_file.to_str().expect("path should be valid UTF-8"),
None,
)],
fixture
.wc_path
.join("test_copy.txt")
.to_str()
.expect("path should be valid UTF-8"),
&mut CopyOptions::new(),
);
assert!(copy_result.is_err());
}
#[test]
fn test_mkdir_multiple() {
let mut fixture = ClientTestFixture::new();
let dir1 = fixture.wc_path.join("dir1");
let dir2 = fixture.wc_path.join("dir2");
let result = fixture.ctx.mkdir_multiple(
&[
dir1.to_str().expect("path should be valid UTF-8"),
dir2.to_str().expect("path should be valid UTF-8"),
],
&mut MkdirOptions::new(),
);
result.unwrap();
assert!(dir1.exists());
assert!(dir2.exists());
}
#[test]
fn test_propget_propset() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "test content");
let propset_result = fixture.ctx.propset(
"test:property",
Some(b"test value"),
test_file.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::new().with_depth(Depth::Empty),
);
propset_result.unwrap();
let props = fixture
.ctx
.propget(
"test:property",
test_file.to_str().expect("path should be valid UTF-8"),
&PropGetOptions::new()
.with_peg_revision(Revision::Working)
.with_revision(Revision::Working)
.with_depth(Depth::Empty),
None, )
.unwrap();
assert!(!props.is_empty(), "Should have at least one property entry");
for (_path, value) in props.iter() {
assert_eq!(value, b"test value");
}
}
#[test]
fn test_propget_with_inherited() {
let mut fixture = ClientTestFixture::new();
let wc_path_str = fixture.wc_path_str().to_string();
fixture
.ctx
.propset(
"test:parent-property",
Some(b"parent value"),
&wc_path_str,
&PropSetOptions::new().with_depth(Depth::Empty),
)
.unwrap();
let subdir = fixture.add_dir("subdir");
let test_file = subdir.join("test.txt");
std::fs::write(&test_file, "test content").unwrap();
fixture.ctx.add(&test_file, &AddOptions::new()).unwrap();
fixture
.ctx
.propset(
"test:file-property",
Some(b"file value"),
test_file.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::new().with_depth(Depth::Empty),
)
.unwrap();
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
let result = fixture.ctx.propget_with_inherited(
"test:parent-property",
test_file_str,
&PropGetOptions::new()
.with_peg_revision(Revision::Working)
.with_revision(Revision::Working)
.with_depth(Depth::Empty),
None,
);
assert!(result.is_ok(), "propget_with_inherited should succeed");
let (props, inherited) = result.unwrap();
assert!(props.is_empty() || !props.contains_key(test_file_str));
assert!(inherited.is_empty() || !inherited.is_empty()); }
#[test]
fn test_move_path() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test_file.txt", "Test content");
fixture.commit();
let new_path = fixture.wc_path.join("renamed_file.txt");
let result = fixture.ctx.move_path(
&[test_file.to_str().expect("path should be valid UTF-8")],
new_path.to_str().expect("path should be valid UTF-8"),
&mut MoveOptions::new(),
);
result.unwrap();
assert!(new_path.exists());
assert!(!test_file.exists());
let content = std::fs::read(&new_path).expect("should read file");
assert_eq!(content, b"Test content");
}
#[test]
fn test_revert() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test_file.txt", "Original content");
fixture.commit();
std::fs::write(&test_file, b"Modified content").expect("should write file");
assert_eq!(
std::fs::read(&test_file).expect("should read file"),
b"Modified content"
);
let result = fixture.ctx.revert(
&[test_file.to_str().expect("path should be valid UTF-8")],
&RevertOptions::new().with_depth(Depth::Empty),
);
result.unwrap();
assert_eq!(
std::fs::read(&test_file).expect("should read file"),
b"Original content"
);
}
#[test]
fn test_resolved() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("conflict_file.txt", "Initial content");
fixture.commit();
let result = fixture.ctx.resolved(
test_file.to_str().expect("path should be valid UTF-8"),
false, );
result.unwrap();
}
#[test]
fn test_proplist_all() {
let mut fixture = ClientTestFixture::new();
let mut prop_count = 0;
let options = ProplistOptions {
peg_revision: Revision::Working,
revision: Revision::Working,
depth: Depth::Empty,
changelists: None,
get_target_inherited_props: false,
};
let result = fixture.ctx.proplist_all(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&options,
&mut |_path, _props| {
prop_count += 1;
Ok(())
},
);
result.unwrap();
}
#[test]
fn test_diff() {
let mut fixture = ClientTestFixture::new();
let mut out_stream = crate::io::Stream::buffered();
let mut err_stream = crate::io::Stream::buffered();
let diff_result = fixture.ctx.diff(
&fixture.url,
&Revision::Head,
&fixture.url,
&Revision::Head,
None, &mut out_stream,
&mut err_stream,
&DiffOptions::new().with_depth(Depth::Infinity),
);
diff_result.unwrap();
}
#[test]
fn test_diff_relative_to_dir() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "line1\n");
fixture.commit();
std::fs::write(&test_file, "line1\nline2\n").unwrap();
let mut out_stream = crate::io::Stream::buffered();
let mut err_stream = crate::io::Stream::buffered();
let wc_path = fixture.wc_path.to_str().unwrap();
let result = fixture.ctx.diff(
wc_path,
&Revision::Number(Revnum(1)),
wc_path,
&Revision::Working,
Some(wc_path),
&mut out_stream,
&mut err_stream,
&DiffOptions::new().with_depth(Depth::Infinity),
);
result.unwrap();
}
#[test]
fn test_diff_builder() {
let mut fixture = ClientTestFixture::new();
let mut out_stream = crate::io::Stream::buffered();
let mut err_stream = crate::io::Stream::buffered();
let result = fixture
.ctx
.diff_builder(
fixture.url.clone(),
Revision::Head,
fixture.url.clone(),
Revision::Head,
)
.depth(Depth::Infinity)
.ignore_ancestry(true)
.use_git_diff_format(true)
.execute(&mut out_stream, &mut err_stream);
result.unwrap();
}
#[test]
fn test_list() {
let mut fixture = ClientTestFixture::new();
let mut entries = Vec::new();
let options = ListOptions {
peg_revision: Revision::Head,
revision: Revision::Head,
patterns: None,
depth: Depth::Infinity,
dirent_fields: subversion_sys::SVN_DIRENT_KIND
| subversion_sys::SVN_DIRENT_SIZE
| subversion_sys::SVN_DIRENT_HAS_PROPS
| subversion_sys::SVN_DIRENT_CREATED_REV
| subversion_sys::SVN_DIRENT_TIME
| subversion_sys::SVN_DIRENT_LAST_AUTHOR,
fetch_locks: false,
include_externals: false,
};
let result = fixture
.ctx
.list(&fixture.url, &options, &mut |path: &str, _dirent, _lock| {
entries.push(path.to_string());
Ok(())
});
result.unwrap();
}
#[test]
fn test_list_builder() {
let mut fixture = ClientTestFixture::new();
let mut entries = Vec::new();
let result = fixture
.ctx
.list_builder(&fixture.url)
.depth(Depth::Files)
.fetch_locks(true)
.patterns(vec!["*.txt".to_string()])
.execute(&mut |path, _dirent, _lock| {
entries.push(path.to_string());
Ok(())
});
result.unwrap();
}
#[test]
fn test_copy_builder() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "test content");
let result = fixture
.ctx
.copy_builder(
fixture
.wc_path
.join("test_copy.txt")
.to_str()
.expect("path should be valid UTF-8"),
)
.add_source(
test_file.to_str().expect("path should be valid UTF-8"),
None,
)
.make_parents(true)
.ignore_externals(true)
.execute();
assert!(result.is_err());
}
#[test]
fn test_info_builder() {
let mut fixture = ClientTestFixture::new();
let mut info_count = 0;
let result = fixture
.ctx
.info_builder(&fixture.url)
.depth(Depth::Empty)
.fetch_excluded(true)
.execute(&|_path, _info| {
info_count += 1;
Ok(())
});
result.unwrap();
}
#[test]
fn test_resolve() {
let mut fixture = ClientTestFixture::new();
let result = fixture.ctx.resolve(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
Depth::Infinity,
crate::ConflictChoice::Postpone,
);
result.unwrap();
}
#[test]
fn test_commit_builder() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let wc_path = td.path().join("wc");
let repos = crate::repos::Repos::create(&repo_path).unwrap();
let fs = repos.fs().unwrap();
let uuid = fs.get_uuid().unwrap();
drop(fs);
drop(repos);
std::fs::create_dir_all(&wc_path).unwrap();
let mut wc_ctx = crate::wc::Context::new().unwrap();
let repo_abs_path = repo_path.canonicalize().unwrap();
let wc_abs_path = wc_path.canonicalize().unwrap();
let url_str = crate::path_to_file_url(&repo_abs_path);
wc_ctx
.ensure_adm(
wc_abs_path.to_str().unwrap(),
&url_str,
&url_str,
&uuid,
crate::Revnum(0),
crate::Depth::Infinity,
)
.unwrap();
let test_file = wc_path.join("test.txt");
std::fs::write(&test_file, "test content").unwrap();
let mut ctx = Context::new().unwrap();
let test_file_abs = test_file.canonicalize().unwrap();
ctx.add(
&test_file_abs,
&AddOptions {
depth: Depth::Empty,
force: false,
no_ignore: false,
no_autoprops: false,
add_parents: false,
},
)
.unwrap();
let builder = CommitBuilder::new(&mut ctx)
.add_target(wc_abs_path.to_str().unwrap())
.depth(Depth::Infinity);
let result = builder.execute(None, &mut |_info| Ok(()));
assert!(result.is_ok(), "Commit failed: {:?}", result.err());
}
#[test]
fn test_log_builder() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("test.txt", "test content");
fixture.commit();
let mut log_entries = Vec::new();
let result = fixture
.ctx
.log_builder()
.add_target(fixture.url.clone())
.add_revision_range(Revision::Number(Revnum(0)), Revision::Head)
.discover_changed_paths(true)
.strict_node_history(false)
.include_merged_revisions(false)
.execute(&|_entry| {
log_entries.push(());
Ok(())
});
assert!(result.is_ok(), "Log failed: {:?}", result.err());
}
#[test]
fn test_update_builder() {
let mut fixture = ClientTestFixture::new();
let result = fixture
.ctx
.update_builder()
.add_path(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
)
.revision(Revision::Head)
.depth(Depth::Infinity)
.depth_is_sticky(false)
.ignore_externals(false)
.allow_unver_obstructions(false)
.adds_as_modification(false)
.make_parents(false)
.execute();
result.unwrap();
}
#[test]
fn test_mkdir_builder() {
let mut fixture = ClientTestFixture::new();
let new_dir = fixture.wc_path.join("new_dir");
let result = fixture
.ctx
.mkdir_builder()
.add_path(new_dir.to_str().expect("path should be valid UTF-8"))
.make_parents(true)
.execute(&mut |_info| Ok(()));
assert!(result.is_ok(), "Mkdir failed: {:?}", result.err());
assert!(new_dir.exists());
}
#[test]
fn test_patch_dry_run() {
let mut ctx = Context::new().unwrap();
let temp_dir = std::env::temp_dir();
let patch_file = temp_dir.join("test.patch");
std::fs::write(
&patch_file,
"--- a/test.txt\n+++ b/test.txt\n@@ -1 +1 @@\n-old\n+new\n",
)
.unwrap();
let wc_dir = temp_dir.join("test_wc_patch");
let _ = std::fs::remove_dir_all(&wc_dir);
std::fs::create_dir_all(&wc_dir).unwrap();
let result = ctx.patch(
&patch_file,
&wc_dir,
&mut PatchOptions::new().with_dry_run(true),
);
let _ = std::fs::remove_file(&patch_file);
let _ = std::fs::remove_dir_all(&wc_dir);
assert!(
result.is_err(),
"Patch should fail for non-working-copy directory"
);
}
#[test]
fn test_relocate_invalid_path() {
let mut ctx = Context::new().unwrap();
let temp_dir = std::env::temp_dir();
let invalid_wc = temp_dir.join("non_existent_wc");
let result = ctx.relocate(
&invalid_wc,
"http://old.example.com/repo",
"http://new.example.com/repo",
false, );
assert!(result.is_err());
}
#[test]
fn test_revprop_operations() {
let mut ctx = Context::new().unwrap();
{
let result =
ctx.revprop_get("svn:author", "file:///non/existent/repo", &Revision::Head);
assert!(result.is_err());
}
{
let result = ctx.revprop_list("file:///non/existent/repo", &Revision::Head);
assert!(result.is_err());
}
{
let result = ctx.revprop_set(
"test:property",
Some(b"test value"),
"file:///non/existent/repo",
&RevpropSetOptions::new().with_revision(Revision::Head),
);
assert!(result.is_err());
}
}
#[test]
fn test_revprop_property_name_validation() {
let mut ctx = Context::new().unwrap();
{
let result =
ctx.revprop_get("svn:author", "file:///non/existent/repo", &Revision::Head);
assert!(
result.is_err(),
"Should fail due to non-existent repo, not property name"
);
}
{
let result = ctx.revprop_set(
"custom:property",
Some(b"test value"),
"file:///non/existent/repo",
&RevpropSetOptions::new().with_revision(Revision::Head),
);
assert!(
result.is_err(),
"Should fail due to non-existent repo, not property name"
);
}
}
#[test]
fn test_revprop_hash_conversion() {
use std::collections::HashMap;
let mut revprops_map = HashMap::new();
revprops_map.insert("svn:author".to_string(), b"test_user".to_vec());
revprops_map.insert("svn:log".to_string(), b"test commit message".to_vec());
revprops_map.insert("custom:prop".to_string(), b"custom value".to_vec());
let blame_info = BlameInfo {
line_no: 1,
revision: Revnum::from(123u64),
revprops: revprops_map.clone(),
merged_revision: None,
merged_revprops: HashMap::new(),
merged_path: None,
line: "test line".to_string(),
local_change: false,
};
assert_eq!(blame_info.revprops.len(), 3);
assert_eq!(blame_info.revprops.get("svn:author").unwrap(), b"test_user");
assert_eq!(
blame_info.revprops.get("svn:log").unwrap(),
b"test commit message"
);
assert_eq!(
blame_info.revprops.get("custom:prop").unwrap(),
b"custom value"
);
let mut ctx = Context::new().unwrap();
let _commit_builder = CommitBuilder::new(&mut ctx)
.add_revprop("svn:author", "test_author")
.add_revprop("svn:log", "test message");
let _mkdir_builder = MkdirBuilder::new(&mut ctx)
.add_revprop("svn:author", b"test_author".to_vec())
.add_revprop("svn:log", b"test message".to_vec());
}
#[test]
fn test_suggest_merge_sources_invalid_url() {
let mut ctx = Context::new().unwrap();
let result = ctx.suggest_merge_sources("file:///non/existent/repo/trunk", &Revision::Head);
assert!(result.is_err());
}
#[test]
fn test_upgrade_invalid_path() {
let mut ctx = Context::new().unwrap();
let temp_dir = std::env::temp_dir();
let invalid_wc = temp_dir.join("non_existent_wc_upgrade");
let result = ctx.upgrade(&invalid_wc);
assert!(result.is_err());
}
#[test]
fn test_cleanup_basic() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let temp_path_str = temp_dir.path().to_str().unwrap();
let options = CleanupOptions {
break_locks: false,
fix_recorded_timestamps: false,
clear_dav_cache: false,
vacuum_pristines: false,
include_externals: false,
};
let result = ctx.cleanup(temp_path_str, &options);
assert!(result.is_err());
}
#[test]
fn test_vacuum_basic() {
let mut ctx = Context::new().unwrap();
let temp_dir = tempfile::tempdir().unwrap();
let temp_path_str = temp_dir.path().to_str().unwrap();
let options = VacuumOptions {
remove_unversioned_items: false,
remove_ignored_items: false,
fix_recorded_timestamps: false,
vacuum_pristines: true,
include_externals: false,
};
let result = ctx.vacuum(temp_path_str, &options);
assert!(result.is_err());
}
#[test]
fn test_merge_reintegrate_invalid_params() {
let mut ctx = Context::new().unwrap();
let temp_dir = std::env::temp_dir();
let invalid_wc = temp_dir.join("non_existent_merge_wc");
let result = ctx.merge_reintegrate(
"file:///non/existent/source",
&Revision::Head,
&invalid_wc,
true, &[], );
assert!(result.is_err());
}
#[test]
fn test_blame_invalid_path() {
let mut ctx = Context::new().unwrap();
let mut blame_info_received = false;
let mut receiver = |info: BlameInfo| -> Result<(), Error<'static>> {
blame_info_received = true;
assert!(info.line_no >= 0);
Ok(())
};
let result = ctx.blame(
"file:///non/existent/file.txt",
&BlameOptions::new()
.with_peg_revision(Revision::Head)
.with_start_revision(Revision::Number(Revnum::from(1u64)))
.with_end_revision(Revision::Head),
&mut receiver,
);
assert!(result.is_err());
}
#[test]
fn test_cat_invalid_path() {
let mut ctx = Context::new().unwrap();
let mut output = Vec::new();
let options = CatOptions {
revision: Revision::Head,
peg_revision: Revision::Head,
expand_keywords: false,
};
let result = ctx.cat("file:///non/existent/file.txt", &mut output, &options);
assert!(result.is_err());
assert!(output.is_empty());
}
#[test]
fn test_blame_api_structure() {
use std::collections::HashMap;
let info = BlameInfo {
line_no: 42,
revision: Revnum::from(123u64),
revprops: HashMap::new(),
merged_revision: None,
merged_revprops: HashMap::new(),
merged_path: None,
line: "test line content".to_string(),
local_change: false,
};
assert_eq!(info.line_no, 42);
assert_eq!(info.revision, Revnum::from(123u64));
assert_eq!(info.line, "test line content");
assert!(!info.local_change);
assert!(info.merged_revision.is_none());
assert!(info.merged_path.is_none());
}
#[test]
fn test_cat_options_structure() {
let options = CatOptions {
revision: Revision::Number(Revnum::from(42u64)),
peg_revision: Revision::Head,
expand_keywords: true,
};
match options.revision {
Revision::Number(n) => assert_eq!(n, Revnum::from(42u64)),
_ => panic!("Expected Number revision"),
}
match options.peg_revision {
Revision::Head => {} _ => panic!("Expected Head revision"),
}
assert!(options.expand_keywords);
let default_options = CatOptions::default();
match default_options.revision {
Revision::Unspecified => {} _ => panic!("Expected Unspecified revision as default"),
}
match default_options.peg_revision {
Revision::Unspecified => {} _ => panic!("Expected Unspecified peg revision as default"),
}
assert!(!default_options.expand_keywords);
}
#[test]
fn test_uuid_from_invalid_url() {
let mut ctx = Context::new().unwrap();
let result = ctx.uuid_from_url("file:///non/existent/repo");
assert!(result.is_err());
}
#[test]
fn test_uuid_from_invalid_path() {
let mut ctx = Context::new().unwrap();
let temp_dir = std::env::temp_dir();
let invalid_path = temp_dir.join("non_existent_wc");
let result = ctx.uuid_from_path(&invalid_path);
assert!(result.is_err());
}
#[test]
fn test_switch_with_ignore_ancestry() {
let mut fixture = ClientTestFixture::new();
let dir1_url = format!("{}/dir1", fixture.url);
let dir2_url = format!("{}/dir2", fixture.url);
fixture
.ctx
.mkdir(
&[&dir1_url, &dir2_url],
&mut MkdirOptions::new().with_make_parents(true),
)
.unwrap();
let wc_path = fixture.temp_dir.path().join("switch_wc");
let dir1_uri = crate::uri::Uri::new(&dir1_url).unwrap();
fixture
.ctx
.checkout(
dir1_uri.clone(),
&wc_path,
&CheckoutOptions {
peg_revision: Revision::Head,
revision: Revision::Head,
depth: Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let dir2_uri = crate::uri::Uri::new(&dir2_url).unwrap();
let result = fixture.ctx.switch(
&wc_path,
dir2_uri.clone(),
&SwitchOptions {
peg_revision: Revision::Head,
revision: Revision::Head,
depth: Depth::Infinity,
depth_is_sticky: false,
ignore_externals: false,
allow_unver_obstructions: false,
ignore_ancestry: false,
},
);
assert!(
result.is_err(),
"Switch without ignore_ancestry should fail for unrelated paths"
);
let result = fixture.ctx.switch(
&wc_path,
dir2_uri.clone(),
&SwitchOptions::new().with_ignore_ancestry(true),
);
assert!(
result.is_ok(),
"Switch with ignore_ancestry=true should succeed: {:?}",
result
);
}
#[test]
fn test_delete_with_commit_callback() {
let mut fixture = ClientTestFixture::new();
let file_path = fixture.add_file("test.txt", "test content");
fixture.commit();
let mut callback = |_info: &crate::CommitInfo| Ok(());
let result = fixture.ctx.delete(
&[file_path.to_str().expect("path should be valid UTF-8")],
std::collections::HashMap::new(),
&mut DeleteOptions::new().with_commit_callback(&mut callback),
);
assert!(
result.is_ok(),
"Delete with callback should succeed: {:?}",
result
);
let file_path2 = fixture.add_file("test2.txt", "test content 2");
fixture.commit();
let result = fixture.ctx.delete(
&[file_path2.to_str().expect("path should be valid UTF-8")],
std::collections::HashMap::new(),
&mut DeleteOptions::new(),
);
assert!(
result.is_ok(),
"Delete without callback should succeed: {:?}",
result
);
}
#[test]
fn test_commit_with_log_message() {
use std::collections::HashMap;
let mut fixture = ClientTestFixture::new();
let filepath = fixture.add_file("test.txt", "content");
let mut revprops = HashMap::new();
revprops.insert("svn:log", "test message");
let result = fixture.ctx.commit(
&[filepath.to_str().expect("path should be valid UTF-8")],
&CommitOptions::default(),
revprops,
None, &mut |_info| Ok(()),
);
match &result {
Ok(()) => println!("Success with svn:log in revprops!"),
Err(e) => {
println!("Error: {:?}", e);
println!("Error code: {}", e.apr_err());
}
}
result.unwrap();
}
#[test]
fn test_copy_with_options() {
let mut fixture = ClientTestFixture::new();
let file_path = fixture.add_file("original.txt", "original content");
fixture.commit();
let copy_path = fixture.wc_path.join("copy1.txt");
let result = fixture.ctx.copy(
&[(
file_path.to_str().expect("path should be valid UTF-8"),
None,
)],
copy_path.to_str().expect("path should be valid UTF-8"),
&mut CopyOptions::new(),
);
assert!(result.is_ok(), "Basic copy should succeed: {:?}", result);
let nested_copy_path = fixture.wc_path.join("subdir/nested/copy2.txt");
let result = fixture.ctx.copy(
&[(
file_path.to_str().expect("path should be valid UTF-8"),
None,
)],
nested_copy_path
.to_str()
.expect("path should be valid UTF-8"),
&mut CopyOptions::new().with_make_parents(true),
);
assert!(
result.is_ok(),
"Copy with make_parents should create intermediate directories: {:?}",
result
);
assert!(nested_copy_path.exists(), "Nested copy should exist");
}
#[test]
fn test_move_with_options() {
let mut fixture = ClientTestFixture::new();
let file_path = fixture.add_file("original.txt", "move test content");
fixture.commit();
let move_path = fixture.wc_path.join("moved.txt");
let result = fixture.ctx.move_path(
&[file_path.to_str().expect("path should be valid UTF-8")],
move_path.to_str().expect("path should be valid UTF-8"),
&mut MoveOptions::new(),
);
assert!(result.is_ok(), "Basic move should succeed: {:?}", result);
assert!(move_path.exists(), "Moved file should exist");
assert!(!file_path.exists(), "Original file should not exist");
let file_path2 = fixture.add_file("file2.txt", "another file");
fixture.commit();
let nested_move_path = fixture.wc_path.join("subdir/nested/moved2.txt");
let result = fixture.ctx.move_path(
&[file_path2.to_str().expect("path should be valid UTF-8")],
nested_move_path
.to_str()
.expect("path should be valid UTF-8"),
&mut MoveOptions::new().with_make_parents(true),
);
assert!(
result.is_ok(),
"Move with make_parents should create intermediate directories: {:?}",
result
);
assert!(nested_move_path.exists(), "Nested moved file should exist");
}
#[test]
fn test_merge_sources_with_options() {
let mut fixture = ClientTestFixture::new();
let file_path = fixture.add_file("file.txt", "initial content");
fixture.commit();
std::fs::write(&file_path, "modified content").expect("should write file");
fixture.commit();
let result = fixture.ctx.merge_sources(
&fixture.url,
&Revision::Number(Revnum(1)),
&fixture.url,
&Revision::Number(Revnum(2)),
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
Depth::Infinity,
&MergeSourcesOptions::new().with_dry_run(true),
);
assert!(
result.is_ok(),
"Merge with dry_run should succeed: {:?}",
result
);
let result = fixture.ctx.merge_sources(
&fixture.url,
&Revision::Number(Revnum(1)),
&fixture.url,
&Revision::Number(Revnum(2)),
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
Depth::Infinity,
&MergeSourcesOptions::new()
.with_ignore_mergeinfo(true)
.with_diff_ignore_ancestry(true),
);
assert!(
result.is_ok(),
"Merge with custom options should succeed: {:?}",
result
);
}
#[test]
fn test_mergeinfo_log() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("file.txt", "initial content");
let mut trunk_rev = Revnum(0);
let wc_path_str = fixture.wc_path_str().to_string();
fixture
.ctx
.commit(
&[&wc_path_str],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |info: &crate::CommitInfo| {
trunk_rev = info.revision();
Ok(())
},
)
.unwrap();
let trunk_url = fixture.url.clone();
let branch_url = format!("{}/branch", fixture.url);
fixture
.ctx
.copy(
&[(trunk_url.as_str(), Some(Revision::Number(trunk_rev)))],
&branch_url,
&mut CopyOptions::new(),
)
.unwrap();
let branch_path = fixture.temp_dir.path().join("branch");
fixture
.ctx
.checkout(
crate::uri::Uri::new(&branch_url).unwrap(),
&branch_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
let file_path = fixture.wc_path.join("file.txt");
std::fs::write(&file_path, "trunk modification").unwrap();
fixture
.ctx
.commit(
&[&wc_path_str],
&CommitOptions::default(),
std::collections::HashMap::new(),
Some(&mut |_items| Ok("test commit message".to_string())),
&mut |_info: &crate::CommitInfo| Ok(()),
)
.unwrap();
fixture
.ctx
.merge_sources(
&trunk_url,
&Revision::Number(trunk_rev),
&trunk_url,
&Revision::Head,
branch_path.to_str().unwrap(),
Depth::Infinity,
&MergeSourcesOptions::new(),
)
.unwrap();
fixture
.ctx
.commit(
&[branch_path.to_str().unwrap()],
&CommitOptions::default(),
std::collections::HashMap::new(),
Some(&mut |_items| Ok("test commit message".to_string())),
&mut |_info: &crate::CommitInfo| Ok(()),
)
.unwrap();
let mut log_count = 0;
let options = MergeinfoLogOptions::new()
.with_finding_merged(true)
.with_discover_changed_paths(true);
let result = fixture
.ctx
.mergeinfo_log(&branch_url, &trunk_url, &options, &mut |_entry| {
log_count += 1;
Ok(())
});
assert!(result.is_ok(), "mergeinfo_log should succeed: {:?}", result);
assert!(log_count > 0, "Should get at least one merge log entry");
}
#[test]
fn test_patch_with_options() {
let mut fixture = ClientTestFixture::new();
let file_path = fixture.add_file("test.txt", "line 1\nline 2\nline 3\n");
fixture.commit();
let patch_path = fixture.temp_dir.path().join("test.patch");
std::fs::write(
&patch_path,
"--- test.txt\n+++ test.txt\n@@ -1,3 +1,3 @@\n line 1\n-line 2\n+line 2 modified\n line 3\n",
)
.unwrap();
let result = fixture.ctx.patch(
&patch_path,
&fixture.wc_path,
&mut PatchOptions::new().with_dry_run(true),
);
assert!(result.is_ok(), "Patch dry_run should succeed: {:?}", result);
let content = std::fs::read_to_string(&file_path).unwrap();
assert_eq!(content, "line 1\nline 2\nline 3\n");
let result = fixture
.ctx
.patch(&patch_path, &fixture.wc_path, &mut PatchOptions::new());
assert!(result.is_ok(), "Patch should succeed: {:?}", result);
let content = std::fs::read_to_string(&file_path).unwrap();
assert!(
content.contains("line 2 modified"),
"File should be patched"
);
}
#[test]
fn test_propset_remote() {
let mut fixture = ClientTestFixture::new();
let _test_file = fixture.add_file("file.txt", "test content");
let mut committed_rev = Revnum(0);
fixture
.ctx
.commit(
&[fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8")],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |info| {
committed_rev = info.revision();
Ok(())
},
)
.unwrap();
let file_url = format!("{}/file.txt", fixture.url);
let mut options = PropSetRemoteOptions::new()
.with_skip_checks(false)
.with_base_revision_for_url(committed_rev);
let result = fixture.ctx.propset_remote(
"test:property",
Some(b"test value"),
&file_url,
&mut options,
);
assert!(
result.is_ok(),
"propset_remote should succeed: {:?}",
result
);
let props_result = fixture.ctx.propget(
"test:property",
&file_url,
&PropGetOptions {
peg_revision: Revision::Head,
revision: Revision::Head,
depth: Depth::Empty,
changelists: None,
},
None,
);
let props = props_result.unwrap();
assert!(!props.is_empty(), "Property should be set");
let prop_val = props.get(&file_url);
assert!(prop_val.is_some());
assert_eq!(prop_val.unwrap(), b"test value");
}
#[test]
fn test_get_changelists() {
let mut fixture = ClientTestFixture::new();
let mut empty_found = Vec::new();
fixture
.ctx
.get_changelists(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
Depth::Infinity,
None,
&mut |path, changelist| {
empty_found.push((path.to_string(), changelist.to_string()));
Ok(())
},
)
.unwrap();
assert_eq!(empty_found.len(), 0);
let file1 = fixture.add_file("file1.txt", "content1");
let file2 = fixture.add_file("file2.txt", "content2");
let file3 = fixture.add_file("file3.txt", "content3");
fixture
.ctx
.add_to_changelist(
&[
file1.to_str().expect("path should be valid UTF-8"),
file2.to_str().expect("path should be valid UTF-8"),
],
"changelist1",
Depth::Empty,
None,
)
.unwrap();
fixture
.ctx
.add_to_changelist(
&[file3.to_str().expect("path should be valid UTF-8")],
"changelist2",
Depth::Empty,
None,
)
.unwrap();
let mut found_paths = Vec::new();
fixture
.ctx
.get_changelists(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
Depth::Infinity,
None,
&mut |path, changelist| {
found_paths.push((path.to_string(), changelist.to_string()));
Ok(())
},
)
.unwrap();
assert_eq!(found_paths.len(), 3);
assert!(found_paths
.iter()
.any(|(p, cl)| p.ends_with("file1.txt") && cl == "changelist1"));
assert!(found_paths
.iter()
.any(|(p, cl)| p.ends_with("file2.txt") && cl == "changelist1"));
assert!(found_paths
.iter()
.any(|(p, cl)| p.ends_with("file3.txt") && cl == "changelist2"));
let mut found_paths2 = Vec::new();
fixture
.ctx
.get_changelists(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
Depth::Infinity,
Some(&["changelist1"]),
&mut |path, changelist| {
found_paths2.push((path.to_string(), changelist.to_string()));
Ok(())
},
)
.unwrap();
assert_eq!(found_paths2.len(), 2);
assert!(found_paths2.iter().all(|(_, cl)| cl == "changelist1"));
}
#[test]
fn test_export() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("file1.txt", "content 1");
fixture.add_dir("subdir");
let file2 = fixture.wc_path.join("subdir/file2.txt");
std::fs::write(&file2, "content 2").unwrap();
fixture.ctx.add(&file2, &AddOptions::new()).unwrap();
fixture.commit();
let export_path = fixture.temp_dir.path().join("export");
let wc_path_str = fixture.wc_path_str().to_string();
let export_opts = ExportOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
overwrite: false,
ignore_externals: false,
ignore_keywords: false,
depth: crate::Depth::Infinity,
native_eol: crate::NativeEOL::Standard,
};
let result = fixture.ctx.export(&wc_path_str, &export_path, &export_opts);
assert!(
result.is_ok(),
"Export from WC should succeed: {:?}",
result
);
assert!(
export_path.join("file1.txt").exists(),
"file1.txt should be exported"
);
assert!(
export_path.join("subdir").exists(),
"subdir should be exported"
);
assert!(
export_path.join("subdir/file2.txt").exists(),
"file2.txt should be exported"
);
assert!(
!export_path.join(".svn").exists(),
".svn should not be exported"
);
assert!(
!export_path.join("subdir/.svn").exists(),
"subdir/.svn should not be exported"
);
let content1 = std::fs::read_to_string(export_path.join("file1.txt")).unwrap();
assert_eq!(content1, "content 1");
let content2 = std::fs::read_to_string(export_path.join("subdir/file2.txt")).unwrap();
assert_eq!(content2, "content 2");
let export_path3 = fixture.temp_dir.path().join("export3");
let shallow_opts = ExportOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
overwrite: false,
ignore_externals: false,
ignore_keywords: false,
depth: crate::Depth::Files,
native_eol: crate::NativeEOL::Standard,
};
let result = fixture
.ctx
.export(&wc_path_str, &export_path3, &shallow_opts);
assert!(
result.is_ok(),
"Shallow export should succeed: {:?}",
result
);
assert!(
export_path3.join("file1.txt").exists(),
"Top-level file should be exported"
);
assert!(
!export_path3.join("subdir").exists(),
"Subdirectory should not be exported with depth=Files"
);
assert!(
!export_path3.join("subdir/file2.txt").exists(),
"Files in subdirectories should not be exported with depth=Files"
);
}
#[test]
fn test_export_depth_infinity_honors_externals() {
let mut fixture = ClientTestFixture::new();
fixture.add_dir("ext-source");
let ext_file = fixture.wc_path.join("ext-source/ext-file.txt");
std::fs::write(&ext_file, "external content").unwrap();
fixture.ctx.add(&ext_file, &AddOptions::new()).unwrap();
fixture.commit();
let wc_path_str = fixture.wc_path_str().to_string();
let update_opts = UpdateOptions {
depth: crate::Depth::Infinity,
depth_is_sticky: false,
ignore_externals: true,
allow_unver_obstructions: false,
adds_as_modifications: false,
make_parents: false,
};
fixture
.ctx
.update(&[&wc_path_str], crate::Revision::Head, &update_opts)
.unwrap();
let externals_val = format!("{}/ext-source ext-link", fixture.url);
fixture
.ctx
.propset(
"svn:externals",
Some(externals_val.as_bytes()),
&wc_path_str,
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let export_path = fixture.temp_dir.path().join("export-ext");
let export_opts = ExportOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
overwrite: false,
ignore_externals: false,
ignore_keywords: false,
depth: crate::Depth::Infinity,
native_eol: crate::NativeEOL::Standard,
};
fixture
.ctx
.export(&fixture.url, &export_path, &export_opts)
.unwrap();
assert!(
export_path.join("ext-link").exists(),
"External directory should be exported when depth=Infinity and ignore_externals=false"
);
assert_eq!(
std::fs::read_to_string(export_path.join("ext-link/ext-file.txt")).unwrap(),
"external content"
);
}
#[test]
fn test_import() {
let mut fixture = ClientTestFixture::new();
let import_path = fixture.temp_dir.path().join("to_import");
std::fs::create_dir(&import_path).unwrap();
std::fs::write(import_path.join("file1.txt"), "import content 1").unwrap();
let subdir = import_path.join("subdir");
std::fs::create_dir(&subdir).unwrap();
std::fs::write(subdir.join("file2.txt"), "import content 2").unwrap();
let import_url_str = format!("{}/imported", fixture.url);
let import_url = crate::uri::Uri::new(&import_url_str).unwrap();
let canonical_url = import_url.canonical();
let mut import_opts = ImportOptions {
depth: crate::Depth::Infinity,
no_ignore: false,
no_autoprops: false,
ignore_unknown_node_types: false,
revprop_table: None,
filter_callback: None,
commit_callback: None,
};
let result = fixture
.ctx
.import(&import_path, canonical_url.as_str(), &mut import_opts);
assert!(result.is_ok(), "Import should succeed: {:?}", result);
let wc_path_str = fixture.wc_path_str().to_string();
fixture
.ctx
.update(
&[&wc_path_str],
crate::Revision::Head,
&UpdateOptions::default(),
)
.unwrap();
assert!(
fixture.wc_path.join("imported/file1.txt").exists(),
"Imported file1.txt should exist"
);
assert!(
fixture.wc_path.join("imported/subdir").exists(),
"Imported subdir should exist"
);
assert!(
fixture.wc_path.join("imported/subdir/file2.txt").exists(),
"Imported file2.txt should exist"
);
let content1 = std::fs::read_to_string(fixture.wc_path.join("imported/file1.txt")).unwrap();
assert_eq!(content1, "import content 1");
let content2 =
std::fs::read_to_string(fixture.wc_path.join("imported/subdir/file2.txt")).unwrap();
assert_eq!(content2, "import content 2");
let import_path2 = fixture.temp_dir.path().join("to_import2");
std::fs::create_dir(&import_path2).unwrap();
std::fs::write(import_path2.join("file3.txt"), "content 3").unwrap();
let subdir2 = import_path2.join("subdir2");
std::fs::create_dir(&subdir2).unwrap();
std::fs::write(subdir2.join("file4.txt"), "content 4").unwrap();
let import_url2_str = format!("{}/imported2", fixture.url);
let import_url2 = crate::uri::Uri::new(&import_url2_str).unwrap();
let canonical_url2 = import_url2.canonical();
let mut import_opts2 = ImportOptions {
depth: crate::Depth::Empty,
no_ignore: false,
no_autoprops: false,
ignore_unknown_node_types: false,
revprop_table: None,
filter_callback: None,
commit_callback: None,
};
let result = fixture
.ctx
.import(&import_path2, canonical_url2.as_str(), &mut import_opts2);
assert!(
result.is_ok(),
"Import with depth=Empty should succeed: {:?}",
result
);
fixture
.ctx
.update(
&[&wc_path_str],
crate::Revision::Head,
&UpdateOptions::default(),
)
.unwrap();
assert!(
fixture.wc_path.join("imported2").exists(),
"imported2 directory should exist"
);
assert!(
!fixture.wc_path.join("imported2/file3.txt").exists(),
"file3.txt should NOT be imported with depth=Empty"
);
assert!(
!fixture.wc_path.join("imported2/subdir2").exists(),
"subdir2 should NOT be imported with depth=Empty"
);
}
#[test]
fn test_lock_unlock() {
let mut fixture = ClientTestFixture::new();
std::env::set_var("USER", "testuser");
let username_provider = crate::auth::get_username_provider();
let mut auth_baton = crate::auth::AuthBaton::open(vec![username_provider]).unwrap();
fixture.ctx.set_auth(&mut auth_baton);
let file1 = fixture.add_file("file1.txt", "lockable content");
fixture.commit();
let file1_path = file1.to_str().expect("path should be valid UTF-8");
let result = fixture
.ctx
.lock(&[file1_path], "Locking for testing", false);
assert!(result.is_ok(), "Lock should succeed: {:?}", result);
let result = fixture
.ctx
.lock(&[file1_path], "Re-locking owned file", false);
assert!(
result.is_ok(),
"Re-locking owned file should succeed: {:?}",
result
);
let result = fixture.ctx.lock(&[file1_path], "Locking with steal", true);
assert!(
result.is_ok(),
"Lock with steal should succeed: {:?}",
result
);
let result = fixture.ctx.unlock(&[file1_path], false);
assert!(result.is_ok(), "Unlock should succeed: {:?}", result);
let result = fixture.ctx.unlock(&[file1_path], false);
assert!(
result.is_err(),
"Unlocking already unlocked file should fail"
);
let result = fixture
.ctx
.lock(&[file1_path], "Locking after unlock", false);
assert!(
result.is_ok(),
"Lock after unlock should succeed: {:?}",
result
);
let result = fixture.ctx.unlock(&[file1_path], true);
assert!(
result.is_ok(),
"Unlock with break_lock should succeed: {:?}",
result
);
}
#[test]
fn test_conflict_walk_no_conflicts() {
let mut fixture = ClientTestFixture::new();
let mut count = 0;
fixture
.ctx
.conflict_walk(&fixture.wc_path, Depth::Infinity, |_conflict| {
count += 1;
Ok(())
})
.unwrap();
assert_eq!(count, 0);
}
#[test]
fn test_conflict_get_no_conflict() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "content");
let conflict = fixture.ctx.conflict_get(&test_file).unwrap();
let (text, props, tree) = conflict.get_conflicted().unwrap();
assert!(!text);
assert!(props.is_empty());
assert!(!tree);
}
#[test]
fn test_conflict_methods_no_conflict() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "content");
let conflict = fixture.ctx.conflict_get(&test_file).unwrap();
let abspath = conflict.get_local_abspath();
assert!(abspath.ends_with("test.txt"));
let (text, props, tree) = conflict.get_conflicted().unwrap();
assert!(!text);
assert!(props.is_empty());
assert!(!tree);
}
#[test]
fn test_conflict_with_text_conflict() {
let mut fixture = ClientTestFixture::new();
let test_file1 = fixture.add_file("test.txt", "line1\nline2\nline3\n");
fixture.commit();
let wc2_path = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
std::fs::write(&test_file1, "line1\nmodified by wc1\nline3\n").unwrap();
fixture.commit();
let test_file2 = wc2_path.join("test.txt");
std::fs::write(&test_file2, "line1\nmodified by wc2\nline3\n").unwrap();
let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.update(&[wc2_str], Revision::Head, &UpdateOptions::default())
.unwrap();
let conflict = fixture.ctx.conflict_get(&test_file2).unwrap();
let (text, _props, _tree) = conflict.get_conflicted().unwrap();
assert!(text, "Should have text conflict");
let abspath = conflict.get_local_abspath();
assert!(abspath.ends_with("test.txt"));
let contents = conflict.text_get_contents().unwrap();
assert!(
contents.0.is_some()
|| contents.1.is_some()
|| contents.2.is_some()
|| contents.3.is_some(),
"Should have at least one content file"
);
let text_res = conflict.text_get_resolution();
assert_eq!(text_res, crate::ClientConflictOptionId::Unspecified);
}
#[test]
fn test_text_conflict_resolution() {
let mut fixture = ClientTestFixture::new();
let test_file1 = fixture.add_file("test.txt", "line1\nline2\nline3\n");
fixture.commit();
let wc2_path = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
std::fs::write(&test_file1, "line1\nmodified by wc1\nline3\n").unwrap();
fixture.commit();
let test_file2 = wc2_path.join("test.txt");
std::fs::write(&test_file2, "line1\nmodified by wc2\nline3\n").unwrap();
let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.update(&[wc2_str], Revision::Head, &UpdateOptions::default())
.unwrap();
let mut conflict = fixture.ctx.conflict_get(&test_file2).unwrap();
let options = conflict
.text_get_resolution_options(&mut fixture.ctx)
.unwrap();
assert!(!options.is_empty(), "Should have resolution options");
let base_option =
ConflictOption::find_by_id(&options, crate::ClientConflictOptionId::MergedText);
assert!(base_option.is_some(), "Should find merged text option");
conflict
.text_resolve_by_id(crate::TextConflictChoice::Merged, &mut fixture.ctx)
.unwrap();
let text_res = conflict.text_get_resolution();
assert_eq!(text_res, crate::ClientConflictOptionId::MergedText);
}
#[test]
fn test_conflict_with_prop_conflict() {
let mut fixture = ClientTestFixture::new();
let test_file1 = fixture.add_file("test.txt", "content\n");
fixture
.ctx
.propset(
"svn:custom",
Some(b"value1"),
test_file1.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let wc2_path = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
fixture
.ctx
.propset(
"svn:custom",
Some(b"value_from_wc1"),
test_file1.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let test_file2 = wc2_path.join("test.txt");
fixture
.ctx
.propset(
"svn:custom",
Some(b"value_from_wc2"),
test_file2.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.update(&[wc2_str], Revision::Head, &UpdateOptions::default())
.unwrap();
let conflict = fixture.ctx.conflict_get(&test_file2).unwrap();
let (text, props, _tree) = conflict.get_conflicted().unwrap();
assert!(!text, "Should not have text conflict");
assert!(!props.is_empty(), "Should have property conflict");
let reject_path = conflict.prop_get_reject_abspath();
assert!(reject_path.is_some(), "Should have reject file path");
}
#[test]
fn test_prop_conflict_resolution() {
let mut fixture = ClientTestFixture::new();
let test_file1 = fixture.add_file("test.txt", "content\n");
fixture
.ctx
.propset(
"svn:custom",
Some(b"initial"),
test_file1.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let wc2_path = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
fixture
.ctx
.propset(
"svn:custom",
Some(b"from_wc1"),
test_file1.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let test_file2 = wc2_path.join("test.txt");
fixture
.ctx
.propset(
"svn:custom",
Some(b"from_wc2"),
test_file2.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
let _ = fixture
.ctx
.update(&[wc2_str], Revision::Head, &UpdateOptions::default());
let mut conflict = fixture.ctx.conflict_get(&test_file2).unwrap();
let propvals = conflict.prop_get_propvals("svn:custom").unwrap();
assert!(propvals.1.is_some(), "Should have working propval");
assert!(propvals.3.is_some(), "Should have incoming new propval");
let options = conflict
.prop_get_resolution_options(&mut fixture.ctx)
.unwrap();
assert!(!options.is_empty(), "Should have resolution options");
conflict
.prop_resolve_by_id(
"svn:custom",
crate::TextConflictChoice::MineConflict,
&mut fixture.ctx,
)
.unwrap();
let prop_res = conflict.prop_get_resolution("svn:custom").unwrap();
assert_eq!(
prop_res,
crate::ClientConflictOptionId::WorkingTextWhereConflicted
);
}
#[test]
fn test_tree_conflict_with_move() {
let mut fixture = ClientTestFixture::new();
let file1 = fixture.add_file("file.txt", "content\n");
fixture.commit();
let wc2_path = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
let file1_moved = fixture.wc_path.join("file_moved.txt");
fixture
.ctx
.move_path(
&[file1.to_str().expect("path should be valid UTF-8")],
file1_moved.to_str().expect("path should be valid UTF-8"),
&mut MoveOptions::default(),
)
.unwrap();
fixture.commit();
let file2 = wc2_path.join("file.txt");
std::fs::write(&file2, "modified content\n").unwrap();
let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
let _ = fixture
.ctx
.update(&[wc2_str], Revision::Head, &UpdateOptions::default());
let mut conflict = fixture.ctx.conflict_get(&file2).unwrap();
let (_text, _props, tree) = conflict.get_conflicted().unwrap();
assert!(
tree,
"Should have tree conflict after deleting file with local modifications"
);
let options = conflict
.tree_get_resolution_options(&mut fixture.ctx)
.unwrap();
assert!(
!options.is_empty(),
"Should have tree conflict resolution options"
);
conflict.tree_get_details(&mut fixture.ctx).unwrap();
let mut has_move_option = false;
for opt in &options {
if let Ok(candidates) = opt.get_moved_to_abspath_candidates() {
if !candidates.is_empty() {
has_move_option = true;
let relpath_candidates = opt.get_moved_to_repos_relpath_candidates().unwrap();
assert!(
!relpath_candidates.is_empty(),
"Should have repository relpath candidates"
);
let mut mutable_opt = ConflictOption {
ptr: opt.ptr,
merged_value_pool: None,
};
mutable_opt
.set_moved_to_abspath(0, &mut fixture.ctx)
.unwrap();
let mut mutable_opt2 = ConflictOption {
ptr: opt.ptr,
merged_value_pool: None,
};
mutable_opt2
.set_moved_to_repos_relpath(0, &mut fixture.ctx)
.unwrap();
break;
}
}
}
let first_option = &options[0];
conflict
.tree_resolve(first_option, &mut fixture.ctx)
.unwrap();
if !has_move_option {
}
}
#[test]
fn test_merge_peg() {
let mut fixture = ClientTestFixture::new();
let trunk_file = fixture.add_file("file.txt", "line 1\n");
fixture.commit();
let branch_url = format!("{}/branch", fixture.url);
fixture
.ctx
.copy(
&[(fixture.url.as_str(), Some(Revision::Head))],
&branch_url,
&mut CopyOptions::new(),
)
.unwrap();
let wc_path_str = fixture.wc_path_str().to_string();
let trunk_url = fixture.url.clone();
fixture
.ctx
.update(&[&wc_path_str], Revision::Head, &UpdateOptions::default())
.unwrap();
fixture
.ctx
.switch(
wc_path_str.as_str(),
branch_url.as_str(),
&SwitchOptions::default(),
)
.unwrap();
std::fs::write(&trunk_file, "line 1\nline 2 from branch\n").unwrap();
fixture.commit();
fixture
.ctx
.switch(
wc_path_str.as_str(),
trunk_url.as_str(),
&SwitchOptions::default(),
)
.unwrap();
let merge_opts = MergeSourcesOptions {
ignore_mergeinfo: false,
diff_ignore_ancestry: false,
force_delete: false,
record_only: false,
dry_run: false,
allow_mixed_rev: true,
merge_options: None,
};
fixture
.ctx
.merge_peg(
&branch_url,
&[],
&Revision::Head,
&wc_path_str,
Depth::Infinity,
&merge_opts,
)
.unwrap();
let content = std::fs::read_to_string(&trunk_file).unwrap();
assert!(
content.contains("line 2 from branch"),
"Merge should have brought in branch changes"
);
}
#[test]
fn test_conflict_option_set_merged_propval() {
let mut fixture = ClientTestFixture::new();
let test_file1 = fixture.add_file("test.txt", "content\n");
fixture
.ctx
.propset(
"custom:prop",
Some(b"value1"),
test_file1.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let wc2_path = fixture.temp_dir.path().canonicalize().unwrap().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
fixture
.ctx
.propset(
"custom:prop",
Some(b"value_from_wc1"),
test_file1.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
fixture.commit();
let test_file2 = wc2_path.join("test.txt");
fixture
.ctx
.propset(
"custom:prop",
Some(b"value_from_wc2"),
test_file2.to_str().expect("path should be valid UTF-8"),
&PropSetOptions::default(),
)
.unwrap();
let wc2_str = wc2_path.to_str().expect("path should be valid UTF-8");
let _ = fixture
.ctx
.update(&[wc2_str], Revision::Head, &UpdateOptions::default());
let test_file2_abs = test_file2.canonicalize().unwrap();
let mut conflict = fixture.ctx.conflict_get(&test_file2_abs).unwrap();
let mut options = conflict
.prop_get_resolution_options(&mut fixture.ctx)
.unwrap();
assert!(!options.is_empty(), "Should have resolution options");
let merged_option = options
.iter_mut()
.find(|opt| opt.get_id() == crate::ClientConflictOptionId::MergedText);
if let Some(option) = merged_option {
option.set_merged_propval(Some(b"custom_merged_value"));
conflict
.prop_resolve_with_option("custom:prop", option, &mut fixture.ctx)
.unwrap();
let mut found_prop = None;
fixture
.ctx
.proplist(
test_file2_abs.to_str().expect("path should be valid UTF-8"),
&ProplistOptions {
peg_revision: Revision::Working,
revision: Revision::Working,
depth: Depth::Empty,
changelists: None,
get_target_inherited_props: false,
},
&mut |_path, props, _inherited| {
if let Some(val) = props.get("custom:prop") {
found_prop = Some(val.clone());
}
Ok(())
},
)
.unwrap();
assert_eq!(
found_prop.as_deref(),
Some(b"custom_merged_value" as &[u8]),
"Merged property value should be applied"
);
}
}
#[test]
fn test_mergeinfo_get_merged() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("test.txt", "initial content\n");
fixture.commit();
let branch_url = format!("{}/branch", fixture.url);
fixture
.ctx
.copy(
&[(fixture.url.as_str(), Some(Revision::Head))],
&branch_url,
&mut CopyOptions::new(),
)
.unwrap();
let branch_wc = fixture.temp_dir.path().join("branch");
let branch_url_obj = crate::uri::Uri::new(&branch_url).unwrap();
fixture
.ctx
.checkout(
branch_url_obj.clone(),
&branch_wc,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
let branch_file2 = branch_wc.join("branch_file.txt");
std::fs::write(&branch_file2, "new file from branch\n").unwrap();
fixture.ctx.add(&branch_file2, &AddOptions::new()).unwrap();
fixture
.ctx
.commit(
&[branch_wc.to_str().expect("path should be valid UTF-8")],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |_| Ok(()),
)
.unwrap();
let wc_path_str = fixture.wc_path_str().to_string();
fixture
.ctx
.update(&[&wc_path_str], Revision::Head, &UpdateOptions::default())
.unwrap();
fixture
.ctx
.merge_peg(
branch_url_obj.as_str(),
&[], &Revision::Head,
&wc_path_str,
Depth::Infinity,
&MergeSourcesOptions {
ignore_mergeinfo: false,
diff_ignore_ancestry: false,
force_delete: false,
record_only: true, dry_run: false,
allow_mixed_rev: true,
merge_options: None,
},
)
.unwrap();
fixture.commit();
let trunk_url = fixture.url.clone();
let mergeinfo = fixture
.ctx
.mergeinfo_get_merged(&trunk_url, &Revision::Head)
.unwrap();
assert!(mergeinfo.is_some(), "Should have mergeinfo after merge");
if let Some(mi) = mergeinfo {
let entries = mi.paths();
assert!(!entries.is_empty(), "Mergeinfo should have entries");
let has_branch = entries.keys().any(|k| k.contains("branch"));
assert!(has_branch, "Mergeinfo should contain the branch URL");
}
let test_file_url = format!("{}/test.txt", trunk_url);
let mergeinfo2 = fixture
.ctx
.mergeinfo_get_merged(&test_file_url, &Revision::Head)
.unwrap();
drop(mergeinfo2);
}
#[test]
fn test_merge() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("test.txt", "line 1\n");
fixture.commit();
let trunk_url = fixture.url.clone();
let branch1_url = format!("{}/branch1", fixture.url);
fixture
.ctx
.copy(
&[(trunk_url.as_str(), Some(Revision::Head))],
&branch1_url,
&mut CopyOptions::new(),
)
.unwrap();
let branch1_wc = fixture.temp_dir.path().join("branch1");
fixture
.ctx
.checkout(
crate::uri::Uri::new(&branch1_url).unwrap(),
&branch1_wc,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
let branch1_file = branch1_wc.join("test.txt");
std::fs::write(&branch1_file, "line 1\nline 2 from branch1\n").unwrap();
fixture
.ctx
.commit(
&[branch1_wc.to_str().unwrap()],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |_| Ok(()),
)
.unwrap();
let branch2_url = format!("{}/branch2", trunk_url);
fixture
.ctx
.copy(
&[(trunk_url.as_str(), Some(Revision::Head))],
&branch2_url,
&mut CopyOptions::new(),
)
.unwrap();
let branch2_wc = fixture.temp_dir.path().join("branch2");
fixture
.ctx
.checkout(
branch2_url.as_str(),
&branch2_wc,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
fixture
.ctx
.merge(
trunk_url.as_str(), &Revision::Head, &branch1_url, &Revision::Head, branch2_wc.to_str().unwrap(), Depth::Infinity,
&MergeSourcesOptions {
ignore_mergeinfo: false,
diff_ignore_ancestry: false,
force_delete: false,
record_only: false,
dry_run: false,
allow_mixed_rev: true,
merge_options: None,
},
)
.unwrap();
let branch2_file = branch2_wc.join("test.txt");
let content = std::fs::read_to_string(&branch2_file).unwrap();
assert!(
content.contains("line 2 from branch1"),
"Merge should have brought in changes from branch1"
);
}
#[test]
fn test_get_merging_summary() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("test.txt", "line 1\n");
fixture.commit();
let trunk_url = fixture.url.clone();
let branch_url = format!("{}/branch", trunk_url);
fixture
.ctx
.copy(
&[(trunk_url.as_str(), Some(Revision::Head))],
&branch_url,
&mut CopyOptions::new(),
)
.unwrap();
let branch_wc = fixture.temp_dir.path().join("branch");
fixture
.ctx
.checkout(
branch_url.as_str(),
&branch_wc,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
let branch_file = branch_wc.join("test.txt");
std::fs::write(&branch_file, "line 1\nline 2 from branch\n").unwrap();
fixture
.ctx
.commit(
&[branch_wc.to_str().unwrap()],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |_| Ok(()),
)
.unwrap();
let wc_path_str = fixture.wc_path_str().to_string();
let (
needs_reintegration,
yca_url,
yca_rev,
_base_url,
base_rev,
right_url,
right_rev,
_target_url,
target_rev,
repos_root,
) = fixture
.ctx
.get_merging_summary(
&branch_url,
&Revision::Head,
&wc_path_str,
&Revision::Working,
)
.unwrap();
assert!(!repos_root.is_empty(), "Should have repository root URL");
assert!(
repos_root.starts_with("file://"),
"Repository root should be a file:// URL"
);
assert!(!yca_url.is_empty(), "Should have YCA URL");
assert!(
yca_url.starts_with(&fixture.url),
"YCA URL should reference the repository"
);
assert!(!right_url.is_empty(), "Should have right URL");
assert!(
right_url.contains("branch"),
"Right URL should reference the branch"
);
assert!(yca_rev >= 0, "YCA revision should be valid");
assert!(base_rev >= 0, "Base revision should be valid");
assert!(right_rev > 0, "Right revision should be > 0");
assert!(target_rev >= 0, "Target revision should be valid");
assert!(
!needs_reintegration,
"Fresh branch should not need reintegration"
);
}
#[test]
fn test_conflict_text_get_contents() {
let mut fixture = ClientTestFixture::new();
let file1 = fixture.add_file("test.txt", "Line 1\nLine 2\nLine 3\n");
fixture.commit();
let wc2 = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(url, &wc2, &ClientTestFixture::default_checkout_options())
.unwrap();
std::fs::write(&file1, "Line 1\nModified by WC1\nLine 3\n").unwrap();
let wc1_str = fixture.wc_path_str().to_string();
fixture
.ctx
.commit(
&[&wc1_str],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |_| Ok(()),
)
.unwrap();
let file2 = wc2.join("test.txt");
std::fs::write(&file2, "Line 1\nModified by WC2\nLine 3\n").unwrap();
let _ = fixture.ctx.update(
&[wc2.to_str().unwrap()],
Revision::Head,
&UpdateOptions::default(),
);
let conflict = fixture.ctx.conflict_get(&file2).unwrap();
let (base_path, working_path, incoming_old, incoming_new_path) =
conflict.text_get_contents().unwrap();
assert!(base_path.is_some(), "Base path should be Some");
assert!(working_path.is_some(), "Working path should be Some");
assert!(
incoming_new_path.is_some(),
"Incoming new path should be Some"
);
let base = base_path.unwrap();
let working = working_path.unwrap();
let incoming_new = incoming_new_path.unwrap();
assert_ne!(base, "", "Base path should not be empty");
assert_ne!(base, "xyzzy", "Base path should not be placeholder");
assert_ne!(working, "", "Working path should not be empty");
assert_ne!(working, "xyzzy", "Working path should not be placeholder");
assert_ne!(incoming_new, "", "Incoming path should not be empty");
assert_ne!(
incoming_new, "xyzzy",
"Incoming path should not be placeholder"
);
assert!(
std::path::Path::new(&base).exists(),
"Base file should exist at path: {}",
base
);
assert!(
std::path::Path::new(&working).exists(),
"Working file should exist at path: {}",
working
);
assert!(
std::path::Path::new(&incoming_new).exists(),
"Incoming file should exist at path: {}",
incoming_new
);
let base_content = std::fs::read_to_string(&base).unwrap();
let working_content = std::fs::read_to_string(&working).unwrap();
let incoming_content = std::fs::read_to_string(&incoming_new).unwrap();
assert_eq!(
base_content, "Line 1\nLine 2\nLine 3\n",
"Base content should match original"
);
assert_eq!(
working_content, "Line 1\nModified by WC2\nLine 3\n",
"Working content should match WC2"
);
assert_eq!(
incoming_content, "Line 1\nModified by WC1\nLine 3\n",
"Incoming content should match WC1"
);
assert!(
incoming_old.is_some(),
"Incoming old should exist for text conflicts"
);
let incoming_old_path = incoming_old.unwrap();
assert_ne!(
incoming_old_path, "",
"Incoming old path should not be empty"
);
assert_ne!(
incoming_old_path, "xyzzy",
"Incoming old path should not be placeholder"
);
assert!(
std::path::Path::new(&incoming_old_path).exists(),
"Incoming old file should exist"
);
let incoming_old_content = std::fs::read_to_string(&incoming_old_path).unwrap();
assert_eq!(
incoming_old_content, "Line 1\nLine 2\nLine 3\n",
"Incoming old should be the base revision"
);
}
#[test]
fn test_conflict_option_get_description() {
let mut fixture = ClientTestFixture::new();
let test_file1 = fixture.add_file("test.txt", "original\n");
fixture.commit();
let wc2_path = fixture.temp_dir.path().join("wc2");
let url = crate::uri::Uri::new(&fixture.url).unwrap();
fixture
.ctx
.checkout(
url,
&wc2_path,
&ClientTestFixture::default_checkout_options(),
)
.unwrap();
std::fs::write(&test_file1, "modified in wc1\n").unwrap();
let wc1_str = fixture.wc_path_str().to_string();
fixture
.ctx
.commit(
&[&wc1_str],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |_| Ok(()),
)
.unwrap();
let test_file2 = wc2_path.join("test.txt");
std::fs::write(&test_file2, "modified in wc2\n").unwrap();
let _ = fixture.ctx.update(
&[wc2_path.to_str().unwrap()],
Revision::Head,
&UpdateOptions::default(),
);
let conflict = fixture.ctx.conflict_get(&test_file2).unwrap();
let options = conflict
.text_get_resolution_options(&mut fixture.ctx)
.unwrap();
assert!(
options.len() > 0,
"Should have at least one resolution option"
);
for option in options.iter() {
let desc = option.get_description();
assert_ne!(desc, "", "Description should not be empty string");
assert_ne!(
desc, "xyzzy",
"Description should not be placeholder 'xyzzy'"
);
assert!(
desc.trim().len() > 3,
"Description '{}' should be meaningful",
desc
);
}
}
#[test]
fn test_status_versioned() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "content\n");
fixture.commit();
std::fs::write(&test_file, "modified content\n").unwrap();
let mut got_status = false;
let test_file_str = test_file.to_str().unwrap().to_string();
let wc_path_str = fixture.wc_path_str().to_string();
fixture
.ctx
.status(
&wc_path_str,
&StatusOptions::default(),
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.versioned(),
true,
"Versioned file should return true"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(
got_status,
"Should have received status for the modified file"
);
}
#[test]
fn test_status_depth_option_affects_behavior() {
let mut fixture = ClientTestFixture::new();
let subdir = fixture.add_dir("subdir");
let subfile = subdir.join("file.txt");
std::fs::write(&subfile, "content").expect("should write file");
fixture.ctx.add(&subfile, &AddOptions::default()).unwrap();
fixture.commit();
std::fs::write(&subfile, "modified").expect("should write file");
let mut paths_empty = Vec::new();
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default().with_depth(Depth::Empty),
&mut |path, _status| {
paths_empty.push(path.to_string());
Ok(())
},
)
.unwrap();
let mut paths_infinity = Vec::new();
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default().with_depth(Depth::Infinity),
&mut |path, _status| {
paths_infinity.push(path.to_string());
Ok(())
},
)
.unwrap();
assert!(
paths_empty.len() < paths_infinity.len(),
"Depth::Empty should report fewer paths ({}) than Depth::Infinity ({})",
paths_empty.len(),
paths_infinity.len()
);
let subfile_str = subfile.to_str().expect("path should be valid UTF-8");
assert!(
paths_infinity.iter().any(|p| p == subfile_str),
"Depth::Infinity should include subdirectory file"
);
assert!(
!paths_empty.iter().any(|p| p == subfile_str),
"Depth::Empty should NOT include subdirectory file"
);
}
#[test]
fn test_status_changed_rev_returns_actual_revision() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "initial content\n");
let mut commit_rev = None;
fixture
.ctx
.commit(
&[fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8")],
&CommitOptions::default(),
std::collections::HashMap::new(),
None,
&mut |info| {
commit_rev = Some(info.revision());
Ok(())
},
)
.unwrap();
let commit_rev = commit_rev.expect("Commit callback should have been called");
let mut got_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
let mut options = StatusOptions::default();
options.get_all = true;
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&options,
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.changed_rev(),
commit_rev,
"Status::changed_rev() should return the actual commit revision"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_status, "Should have received status for the file");
}
#[test]
fn test_add_with_add_parents_option() {
let mut fixture = ClientTestFixture::new();
let nested_dir = fixture.wc_path.join("subdir1").join("subdir2");
std::fs::create_dir_all(&nested_dir).expect("should create dirs");
let nested_file = nested_dir.join("test.txt");
std::fs::write(&nested_file, "nested content\n").expect("should write file");
let options = AddOptions::default().with_add_parents(true);
fixture.ctx.add(&nested_file, &options).unwrap();
let mut added_paths = Vec::new();
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if status.node_status() == crate::StatusKind::Added {
added_paths.push(path.to_string());
}
Ok(())
},
)
.unwrap();
let subdir1_path = fixture
.wc_path
.join("subdir1")
.to_str()
.expect("path should be valid UTF-8")
.to_string();
let subdir2_path = fixture
.wc_path
.join("subdir1")
.join("subdir2")
.to_str()
.expect("path should be valid UTF-8")
.to_string();
let file_path = nested_file
.to_str()
.expect("path should be valid UTF-8")
.to_string();
assert!(
added_paths.contains(&subdir1_path),
"Parent directory subdir1 should be added"
);
assert!(
added_paths.contains(&subdir2_path),
"Parent directory subdir2 should be added"
);
assert!(added_paths.contains(&file_path), "File should be added");
}
#[test]
fn test_revert_with_metadata_only_option() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "original content\n");
fixture.commit();
std::fs::write(&test_file, "modified content\n").expect("should write file");
fixture
.ctx
.propset(
"test-prop",
Some("test-value".as_bytes()),
test_file.to_str().expect("path should be valid UTF-8"),
&crate::client::PropSetOptions::default(),
)
.unwrap();
let options = RevertOptions::default().with_metadata_only(true);
fixture
.ctx
.revert(
&[test_file.to_str().expect("path should be valid UTF-8")],
&options,
)
.unwrap();
let content = std::fs::read_to_string(&test_file).expect("should read file");
assert_eq!(
content, "modified content\n",
"With metadata_only=true, file content should NOT be reverted"
);
let prop_val = fixture
.ctx
.propget(
"test-prop",
test_file.to_str().expect("path should be valid UTF-8"),
&crate::client::PropGetOptions::default(),
None,
)
.unwrap();
assert_eq!(
prop_val.len(),
0,
"With metadata_only=true, property should be reverted"
);
fixture
.ctx
.propset(
"test-prop",
Some("test-value".as_bytes()),
test_file.to_str().expect("path should be valid UTF-8"),
&crate::client::PropSetOptions::default(),
)
.unwrap();
let options = RevertOptions::default().with_metadata_only(false);
fixture
.ctx
.revert(
&[test_file.to_str().expect("path should be valid UTF-8")],
&options,
)
.unwrap();
let content = std::fs::read_to_string(&test_file).expect("should read file");
assert_eq!(
content, "original content\n",
"With metadata_only=false, file content should be reverted"
);
let prop_val = fixture
.ctx
.propget(
"test-prop",
test_file.to_str().expect("path should be valid UTF-8"),
&crate::client::PropGetOptions::default(),
None,
)
.unwrap();
assert_eq!(
prop_val.len(),
0,
"With metadata_only=false, property should be reverted"
);
}
#[test]
fn test_status_conflicted_returns_false_for_normal_files() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "content\n");
fixture.commit();
std::fs::write(&test_file, "modified content\n").expect("should write file");
let mut got_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
let mut options = StatusOptions::default();
options.get_all = true;
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&options,
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.conflicted(),
false,
"Non-conflicted file should return false from conflicted()"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_status, "Should have received status for the file");
}
#[test]
fn test_get_repos_root_returns_actual_values() {
let mut fixture = ClientTestFixture::new();
let (root_url, uuid) = fixture
.ctx
.get_repos_root(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
)
.unwrap();
assert_eq!(
root_url, fixture.url,
"Repository root should match the checkout URL"
);
assert!(!uuid.is_empty(), "Repository UUID should not be empty");
assert_ne!(
uuid, "xyzzy",
"Repository UUID should be an actual UUID, not 'xyzzy'"
);
assert!(
uuid.contains('-'),
"Repository UUID should be in UUID format with hyphens"
);
}
#[test]
fn test_status_local_abspath_returns_actual_path() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("myfile.txt", "content\n");
let mut got_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if path == test_file_str {
let abspath = status.local_abspath_native();
assert_eq!(
abspath,
std::path::Path::new(test_file_str),
"Status::local_abspath_native() should return the actual file path"
);
assert_ne!(
status.local_abspath(),
"xyzzy",
"local_abspath should not be 'xyzzy'"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_status, "Should have received status for the file");
}
#[test]
fn test_status_versioned_returns_false_for_unversioned() {
let mut fixture = ClientTestFixture::new();
let unversioned_file = fixture.wc_path.join("unversioned.txt");
std::fs::write(&unversioned_file, "not tracked\n").expect("should write file");
let mut got_status = false;
let unversioned_str = unversioned_file
.to_str()
.expect("path should be valid UTF-8");
let mut options = StatusOptions::default();
options.get_all = true; fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&options,
&mut |path, status| {
if path == unversioned_str {
assert_eq!(
status.versioned(),
false,
"Unversioned file should return false from versioned()"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(
got_status,
"Should have received status for unversioned file"
);
}
#[test]
fn test_status_wc_is_locked_returns_false_for_unlocked() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "content\n");
let mut got_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.wc_is_locked(),
false,
"Normal unlocked working copy should return false from wc_is_locked()"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_status, "Should have received status for the file");
}
#[test]
fn test_status_file_external_returns_false_for_normal_files() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("normal.txt", "normal file\n");
let mut got_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.file_external(),
false,
"Normal file should return false from file_external()"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_status, "Should have received status for the file");
}
#[test]
fn test_status_kind_returns_actual_kind() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("test.txt", "test content\n");
let test_dir = fixture.add_dir("test_dir");
let mut got_file_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.kind(),
crate::NodeKind::File,
"File should return NodeKind::File from kind()"
);
got_file_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_file_status, "Should have received status for the file");
let mut got_dir_status = false;
let test_dir_str = test_dir.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if path == test_dir_str {
assert_eq!(
status.kind(),
crate::NodeKind::Dir,
"Directory should return NodeKind::Dir from kind()"
);
got_dir_status = true;
}
Ok(())
},
)
.unwrap();
assert!(
got_dir_status,
"Should have received status for the directory"
);
}
#[test]
fn test_status_copied_returns_false_for_normal_files() {
let mut fixture = ClientTestFixture::new();
let test_file = fixture.add_file("normal.txt", "normal file\n");
let mut got_status = false;
let test_file_str = test_file.to_str().expect("path should be valid UTF-8");
fixture
.ctx
.status(
fixture
.wc_path
.to_str()
.expect("path should be valid UTF-8"),
&StatusOptions::default(),
&mut |path, status| {
if path == test_file_str {
assert_eq!(
status.copied(),
false,
"Normal (non-copied) file should return false from copied()"
);
got_status = true;
}
Ok(())
},
)
.unwrap();
assert!(got_status, "Should have received status for the file");
}
#[test]
fn test_iter_logs() {
let mut fixture = ClientTestFixture::new();
fixture.add_file("file1.txt", "first");
let rev1 = fixture.commit();
fixture.add_file("file2.txt", "second");
let rev2 = fixture.commit();
let entries: Vec<_> = fixture
.ctx
.iter_logs(
&[&fixture.url],
&[RevisionRange::new(
Revision::Number(rev1),
Revision::Number(rev2),
)],
&LogOptions::new(),
)
.collect();
assert_eq!(entries.len(), 2, "Should have 2 log entries");
for entry in &entries {
assert!(
entry.is_ok(),
"Entry should be Ok: {:?}",
entry.as_ref().err()
);
}
let revs: Vec<_> = entries
.iter()
.map(|e| e.as_ref().unwrap().revision().unwrap())
.collect();
assert!(revs.contains(&rev2));
assert!(revs.contains(&rev1));
}
#[test]
fn test_iter_logs_early_drop() {
let mut fixture = ClientTestFixture::new();
for i in 0..5 {
fixture.add_file(&format!("file{}.txt", i), &format!("content {}", i));
fixture.commit();
}
let entries: Vec<_> = fixture
.ctx
.iter_logs(
&[&fixture.url],
&[RevisionRange::new(
Revision::Number(Revnum(1)),
Revision::Head,
)],
&LogOptions::new(),
)
.take(2)
.collect();
assert_eq!(entries.len(), 2);
}
#[test]
fn test_iter_logs_error() {
let mut fixture = ClientTestFixture::new();
let mut iter = fixture.ctx.iter_logs(
&[&fixture.url],
&[RevisionRange::new(
Revision::Number(Revnum(0)),
Revision::Number(Revnum(1000)),
)],
&LogOptions::new(),
);
let result = iter.next();
assert!(
result.is_some(),
"iter_logs should yield an error, not be empty"
);
match result.unwrap() {
Ok(_) => panic!("expected an error, got Ok"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("1000"),
"error should mention the invalid revision: {}",
msg
);
}
}
}
}