use crate::{svn_result, with_tmp_pool, Error};
use std::marker::PhantomData;
use subversion_sys::{svn_wc_context_t, svn_wc_version};
fn box_cancel_baton(f: Box<dyn Fn() -> Result<(), Error<'static>>>) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_notify_baton(f: Box<dyn Fn(&Notify)>) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_conflict_baton(
f: Box<
dyn Fn(
&crate::conflict::ConflictDescription,
) -> Result<crate::conflict::ConflictResult, Error<'static>>,
>,
) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_external_baton(
f: Box<dyn Fn(&str, Option<&str>, Option<&str>, crate::Depth) -> Result<(), Error<'static>>>,
) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_fetch_dirents_baton(
f: Box<
dyn Fn(
&str,
&str,
)
-> Result<std::collections::HashMap<String, crate::DirEntry>, Error<'static>>,
>,
) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
fn box_cancel_baton_borrowed(f: &dyn Fn() -> Result<(), Error<'static>>) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
pub(crate) fn box_notify_baton_borrowed(f: &dyn Fn(&Notify)) -> *mut std::ffi::c_void {
Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
}
unsafe fn drop_cancel_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
));
}
unsafe fn drop_notify_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(baton as *mut Box<dyn Fn(&Notify)>));
}
unsafe fn drop_conflict_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton
as *mut Box<
dyn Fn(
&crate::conflict::ConflictDescription,
) -> Result<crate::conflict::ConflictResult, Error<'static>>,
>,
));
}
unsafe fn drop_external_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton
as *mut Box<
dyn Fn(
&str,
Option<&str>,
Option<&str>,
crate::Depth,
) -> Result<(), Error<'static>>,
>,
));
}
unsafe fn drop_fetch_dirents_baton(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton
as *mut Box<
dyn Fn(
&str,
&str,
) -> Result<
std::collections::HashMap<String, crate::DirEntry>,
Error<'static>,
>,
>,
));
}
unsafe fn drop_cancel_baton_borrowed(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(
baton as *mut &dyn Fn() -> Result<(), Error<'static>>,
));
}
pub(crate) unsafe fn drop_notify_baton_borrowed(baton: *mut std::ffi::c_void) {
drop(Box::from_raw(baton as *mut &dyn Fn(&Notify)));
}
pub fn version() -> crate::Version {
unsafe { crate::Version(svn_wc_version()) }
}
pub const STATUS_NONE: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_none as u32;
pub const STATUS_UNVERSIONED: u32 =
subversion_sys::svn_wc_status_kind_svn_wc_status_unversioned as u32;
pub const STATUS_NORMAL: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_normal as u32;
pub const STATUS_ADDED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_added as u32;
pub const STATUS_MISSING: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_missing as u32;
pub const STATUS_DELETED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_deleted as u32;
pub const STATUS_REPLACED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_replaced as u32;
pub const STATUS_MODIFIED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_modified as u32;
pub const STATUS_MERGED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_merged as u32;
pub const STATUS_CONFLICTED: u32 =
subversion_sys::svn_wc_status_kind_svn_wc_status_conflicted as u32;
pub const STATUS_IGNORED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_ignored as u32;
pub const STATUS_OBSTRUCTED: u32 =
subversion_sys::svn_wc_status_kind_svn_wc_status_obstructed as u32;
pub const STATUS_EXTERNAL: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_external as u32;
pub const STATUS_INCOMPLETE: u32 =
subversion_sys::svn_wc_status_kind_svn_wc_status_incomplete as u32;
pub const SCHEDULE_NORMAL: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal as u32;
pub const SCHEDULE_ADD: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add as u32;
pub const SCHEDULE_DELETE: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete as u32;
pub const SCHEDULE_REPLACE: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_replace as u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum StatusKind {
None = subversion_sys::svn_wc_status_kind_svn_wc_status_none as u32,
Unversioned = subversion_sys::svn_wc_status_kind_svn_wc_status_unversioned as u32,
Normal = subversion_sys::svn_wc_status_kind_svn_wc_status_normal as u32,
Added = subversion_sys::svn_wc_status_kind_svn_wc_status_added as u32,
Missing = subversion_sys::svn_wc_status_kind_svn_wc_status_missing as u32,
Deleted = subversion_sys::svn_wc_status_kind_svn_wc_status_deleted as u32,
Replaced = subversion_sys::svn_wc_status_kind_svn_wc_status_replaced as u32,
Modified = subversion_sys::svn_wc_status_kind_svn_wc_status_modified as u32,
Merged = subversion_sys::svn_wc_status_kind_svn_wc_status_merged as u32,
Conflicted = subversion_sys::svn_wc_status_kind_svn_wc_status_conflicted as u32,
Ignored = subversion_sys::svn_wc_status_kind_svn_wc_status_ignored as u32,
Obstructed = subversion_sys::svn_wc_status_kind_svn_wc_status_obstructed as u32,
External = subversion_sys::svn_wc_status_kind_svn_wc_status_external as u32,
Incomplete = subversion_sys::svn_wc_status_kind_svn_wc_status_incomplete as u32,
}
impl From<subversion_sys::svn_wc_status_kind> for StatusKind {
fn from(status: subversion_sys::svn_wc_status_kind) -> Self {
match status {
subversion_sys::svn_wc_status_kind_svn_wc_status_none => StatusKind::None,
subversion_sys::svn_wc_status_kind_svn_wc_status_unversioned => StatusKind::Unversioned,
subversion_sys::svn_wc_status_kind_svn_wc_status_normal => StatusKind::Normal,
subversion_sys::svn_wc_status_kind_svn_wc_status_added => StatusKind::Added,
subversion_sys::svn_wc_status_kind_svn_wc_status_missing => StatusKind::Missing,
subversion_sys::svn_wc_status_kind_svn_wc_status_deleted => StatusKind::Deleted,
subversion_sys::svn_wc_status_kind_svn_wc_status_replaced => StatusKind::Replaced,
subversion_sys::svn_wc_status_kind_svn_wc_status_modified => StatusKind::Modified,
subversion_sys::svn_wc_status_kind_svn_wc_status_merged => StatusKind::Merged,
subversion_sys::svn_wc_status_kind_svn_wc_status_conflicted => StatusKind::Conflicted,
subversion_sys::svn_wc_status_kind_svn_wc_status_ignored => StatusKind::Ignored,
subversion_sys::svn_wc_status_kind_svn_wc_status_obstructed => StatusKind::Obstructed,
subversion_sys::svn_wc_status_kind_svn_wc_status_external => StatusKind::External,
subversion_sys::svn_wc_status_kind_svn_wc_status_incomplete => StatusKind::Incomplete,
_ => unreachable!("unknown svn_wc_status_kind value: {}", status),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropChange {
pub name: String,
pub value: Option<Vec<u8>>,
}
pub struct Status<'pool> {
ptr: *const subversion_sys::svn_wc_status3_t,
_pool: apr::pool::PoolHandle<'pool>,
}
impl<'pool> Status<'pool> {
pub fn node_status(&self) -> StatusKind {
unsafe { (*self.ptr).node_status.into() }
}
pub fn text_status(&self) -> StatusKind {
unsafe { (*self.ptr).text_status.into() }
}
pub fn prop_status(&self) -> StatusKind {
unsafe { (*self.ptr).prop_status.into() }
}
pub fn copied(&self) -> bool {
unsafe { (*self.ptr).copied != 0 }
}
pub fn switched(&self) -> bool {
unsafe { (*self.ptr).switched != 0 }
}
pub fn locked(&self) -> bool {
unsafe { (*self.ptr).locked != 0 }
}
pub fn revision(&self) -> crate::Revnum {
unsafe { crate::Revnum((*self.ptr).revision) }
}
pub fn changed_rev(&self) -> crate::Revnum {
unsafe { crate::Revnum((*self.ptr).changed_rev) }
}
pub fn repos_relpath(&self) -> Option<String> {
unsafe {
if (*self.ptr).repos_relpath.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).repos_relpath)
.to_string_lossy()
.into_owned(),
)
}
}
}
pub fn kind(&self) -> i32 {
unsafe { (*self.ptr).kind as i32 }
}
pub fn depth(&self) -> i32 {
unsafe { (*self.ptr).depth }
}
pub fn filesize(&self) -> i64 {
unsafe { (*self.ptr).filesize }
}
pub fn versioned(&self) -> bool {
unsafe { (*self.ptr).versioned != 0 }
}
pub fn repos_uuid(&self) -> Option<String> {
unsafe {
if (*self.ptr).repos_uuid.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).repos_uuid)
.to_string_lossy()
.into_owned(),
)
}
}
}
pub fn repos_root_url(&self) -> Option<String> {
unsafe {
if (*self.ptr).repos_root_url.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).repos_root_url)
.to_string_lossy()
.into_owned(),
)
}
}
}
pub fn dup(&self) -> Status<'static> {
let pool = apr::pool::Pool::new();
let ptr = unsafe { subversion_sys::svn_wc_dup_status3(self.ptr, pool.as_mut_ptr()) };
Status {
ptr,
_pool: apr::pool::PoolHandle::Owned(pool),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum Schedule {
Normal = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal as u32,
Add = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add as u32,
Delete = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete as u32,
Replace = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_replace as u32,
}
impl From<subversion_sys::svn_wc_schedule_t> for Schedule {
fn from(schedule: subversion_sys::svn_wc_schedule_t) -> Self {
match schedule {
subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal => Schedule::Normal,
subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add => Schedule::Add,
subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete => Schedule::Delete,
subversion_sys::svn_wc_schedule_t_svn_wc_schedule_replace => Schedule::Replace,
_ => unreachable!("unknown svn_wc_schedule_t value: {}", schedule),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MergeOutcome {
Unchanged,
Merged,
Conflict,
NoMerge,
}
impl From<subversion_sys::svn_wc_merge_outcome_t> for MergeOutcome {
fn from(outcome: subversion_sys::svn_wc_merge_outcome_t) -> Self {
match outcome {
subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_unchanged => {
MergeOutcome::Unchanged
}
subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_merged => MergeOutcome::Merged,
subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_conflict => MergeOutcome::Conflict,
subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_no_merge => MergeOutcome::NoMerge,
_ => unreachable!("unknown svn_wc_merge_outcome_t value: {}", outcome),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NotifyState {
Inapplicable,
Unknown,
Unchanged,
Missing,
Obstructed,
Changed,
Merged,
Conflicted,
SourceMissing,
}
impl From<subversion_sys::svn_wc_notify_state_t> for NotifyState {
fn from(state: subversion_sys::svn_wc_notify_state_t) -> Self {
match state {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable => {
NotifyState::Inapplicable
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unknown => {
NotifyState::Unknown
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unchanged => {
NotifyState::Unchanged
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_missing => {
NotifyState::Missing
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_obstructed => {
NotifyState::Obstructed
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_changed => {
NotifyState::Changed
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_merged => NotifyState::Merged,
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_conflicted => {
NotifyState::Conflicted
}
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_source_missing => {
NotifyState::SourceMissing
}
_ => unreachable!("unknown svn_wc_notify_state_t value: {}", state),
}
}
}
impl From<NotifyState> for subversion_sys::svn_wc_notify_state_t {
fn from(state: NotifyState) -> Self {
match state {
NotifyState::Inapplicable => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable
}
NotifyState::Unknown => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unknown
}
NotifyState::Unchanged => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unchanged
}
NotifyState::Missing => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_missing
}
NotifyState::Obstructed => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_obstructed
}
NotifyState::Changed => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_changed
}
NotifyState::Merged => subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_merged,
NotifyState::Conflicted => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_conflicted
}
NotifyState::SourceMissing => {
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_source_missing
}
}
}
}
pub struct FileChange<'a> {
pub path: &'a str,
pub tmpfile1: Option<&'a str>,
pub tmpfile2: Option<&'a str>,
pub rev1: crate::Revnum,
pub rev2: crate::Revnum,
pub mimetype1: Option<&'a str>,
pub mimetype2: Option<&'a str>,
pub prop_changes: Vec<PropChange>,
}
unsafe fn diff_prop_array_to_vec(arr: *const apr_sys::apr_array_header_t) -> Vec<PropChange> {
if arr.is_null() {
return Vec::new();
}
let typed = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::from_ptr(
arr as *mut apr_sys::apr_array_header_t,
);
typed
.iter()
.map(|p| {
let name = if p.name.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(p.name)
.to_string_lossy()
.into_owned()
};
let value = if p.value.is_null() {
None
} else {
Some(crate::svn_string_helpers::to_vec(&*p.value))
};
PropChange { name, value }
})
.collect()
}
unsafe fn diff_opt_str<'a>(p: *const std::os::raw::c_char) -> Option<&'a str> {
if p.is_null() {
None
} else {
std::ffi::CStr::from_ptr(p).to_str().ok()
}
}
unsafe extern "C" fn diff_cb_file_opened(
tree_conflicted: *mut subversion_sys::svn_boolean_t,
skip: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
rev: subversion_sys::svn_revnum_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
match cb.file_opened(path, crate::Revnum(rev)) {
Ok((tc, sk)) => {
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
if !skip.is_null() {
*skip = sk as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_file_changed(
contentstate: *mut subversion_sys::svn_wc_notify_state_t,
propstate: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
tmpfile1: *const std::os::raw::c_char,
tmpfile2: *const std::os::raw::c_char,
rev1: subversion_sys::svn_revnum_t,
rev2: subversion_sys::svn_revnum_t,
mimetype1: *const std::os::raw::c_char,
mimetype2: *const std::os::raw::c_char,
propchanges: *const apr_sys::apr_array_header_t,
_originalprops: *mut apr_sys::apr_hash_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let change = FileChange {
path: std::ffi::CStr::from_ptr(path).to_str().unwrap_or(""),
tmpfile1: diff_opt_str(tmpfile1),
tmpfile2: diff_opt_str(tmpfile2),
rev1: crate::Revnum(rev1),
rev2: crate::Revnum(rev2),
mimetype1: diff_opt_str(mimetype1),
mimetype2: diff_opt_str(mimetype2),
prop_changes: diff_prop_array_to_vec(propchanges),
};
match cb.file_changed(&change) {
Ok((cs, ps, tc)) => {
if !contentstate.is_null() {
*contentstate = cs.into();
}
if !propstate.is_null() {
*propstate = ps.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_file_added(
contentstate: *mut subversion_sys::svn_wc_notify_state_t,
propstate: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
tmpfile1: *const std::os::raw::c_char,
tmpfile2: *const std::os::raw::c_char,
rev1: subversion_sys::svn_revnum_t,
rev2: subversion_sys::svn_revnum_t,
mimetype1: *const std::os::raw::c_char,
mimetype2: *const std::os::raw::c_char,
copyfrom_path: *const std::os::raw::c_char,
copyfrom_revision: subversion_sys::svn_revnum_t,
propchanges: *const apr_sys::apr_array_header_t,
_originalprops: *mut apr_sys::apr_hash_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let change = FileChange {
path: std::ffi::CStr::from_ptr(path).to_str().unwrap_or(""),
tmpfile1: diff_opt_str(tmpfile1),
tmpfile2: diff_opt_str(tmpfile2),
rev1: crate::Revnum(rev1),
rev2: crate::Revnum(rev2),
mimetype1: diff_opt_str(mimetype1),
mimetype2: diff_opt_str(mimetype2),
prop_changes: diff_prop_array_to_vec(propchanges),
};
match cb.file_added(
&change,
diff_opt_str(copyfrom_path),
crate::Revnum(copyfrom_revision),
) {
Ok((cs, ps, tc)) => {
if !contentstate.is_null() {
*contentstate = cs.into();
}
if !propstate.is_null() {
*propstate = ps.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_file_deleted(
state: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
tmpfile1: *const std::os::raw::c_char,
tmpfile2: *const std::os::raw::c_char,
mimetype1: *const std::os::raw::c_char,
mimetype2: *const std::os::raw::c_char,
_originalprops: *mut apr_sys::apr_hash_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
match cb.file_deleted(
path,
diff_opt_str(tmpfile1),
diff_opt_str(tmpfile2),
diff_opt_str(mimetype1),
diff_opt_str(mimetype2),
) {
Ok((st, tc)) => {
if !state.is_null() {
*state = st.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_dir_deleted(
state: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
match cb.dir_deleted(path) {
Ok((st, tc)) => {
if !state.is_null() {
*state = st.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_dir_opened(
tree_conflicted: *mut subversion_sys::svn_boolean_t,
skip: *mut subversion_sys::svn_boolean_t,
skip_children: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
rev: subversion_sys::svn_revnum_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
match cb.dir_opened(path, crate::Revnum(rev)) {
Ok((tc, sk, skc)) => {
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
if !skip.is_null() {
*skip = sk as i32;
}
if !skip_children.is_null() {
*skip_children = skc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_dir_added(
state: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
skip: *mut subversion_sys::svn_boolean_t,
skip_children: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
rev: subversion_sys::svn_revnum_t,
copyfrom_path: *const std::os::raw::c_char,
copyfrom_revision: subversion_sys::svn_revnum_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
match cb.dir_added(
path,
crate::Revnum(rev),
diff_opt_str(copyfrom_path),
crate::Revnum(copyfrom_revision),
) {
Ok((st, tc, sk, skc)) => {
if !state.is_null() {
*state = st.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
if !skip.is_null() {
*skip = sk as i32;
}
if !skip_children.is_null() {
*skip_children = skc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_dir_props_changed(
propstate: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
dir_was_added: subversion_sys::svn_boolean_t,
propchanges: *const apr_sys::apr_array_header_t,
_original_props: *mut apr_sys::apr_hash_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
let changes = diff_prop_array_to_vec(propchanges);
match cb.dir_props_changed(path, dir_was_added != 0, &changes) {
Ok((ps, tc)) => {
if !propstate.is_null() {
*propstate = ps.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
unsafe extern "C" fn diff_cb_dir_closed(
contentstate: *mut subversion_sys::svn_wc_notify_state_t,
propstate: *mut subversion_sys::svn_wc_notify_state_t,
tree_conflicted: *mut subversion_sys::svn_boolean_t,
path: *const std::os::raw::c_char,
dir_was_added: subversion_sys::svn_boolean_t,
diff_baton: *mut std::ffi::c_void,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
match cb.dir_closed(path, dir_was_added != 0) {
Ok((cs, ps, tc)) => {
if !contentstate.is_null() {
*contentstate = cs.into();
}
if !propstate.is_null() {
*propstate = ps.into();
}
if !tree_conflicted.is_null() {
*tree_conflicted = tc as i32;
}
std::ptr::null_mut()
}
Err(e) => e.into_raw(),
}
}
fn make_diff_callbacks4() -> subversion_sys::svn_wc_diff_callbacks4_t {
subversion_sys::svn_wc_diff_callbacks4_t {
file_opened: Some(diff_cb_file_opened),
file_changed: Some(diff_cb_file_changed),
file_added: Some(diff_cb_file_added),
file_deleted: Some(diff_cb_file_deleted),
dir_deleted: Some(diff_cb_dir_deleted),
dir_opened: Some(diff_cb_dir_opened),
dir_added: Some(diff_cb_dir_added),
dir_props_changed: Some(diff_cb_dir_props_changed),
dir_closed: Some(diff_cb_dir_closed),
}
}
pub trait DiffCallbacks {
fn file_opened(
&mut self,
path: &str,
rev: crate::Revnum,
) -> Result<(bool, bool), crate::Error<'static>>;
fn file_changed(
&mut self,
change: &FileChange<'_>,
) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>>;
fn file_added(
&mut self,
change: &FileChange<'_>,
copyfrom_path: Option<&str>,
copyfrom_revision: crate::Revnum,
) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>>;
fn file_deleted(
&mut self,
path: &str,
tmpfile1: Option<&str>,
tmpfile2: Option<&str>,
mimetype1: Option<&str>,
mimetype2: Option<&str>,
) -> Result<(NotifyState, bool), crate::Error<'static>>;
fn dir_deleted(&mut self, path: &str) -> Result<(NotifyState, bool), crate::Error<'static>>;
fn dir_opened(
&mut self,
path: &str,
rev: crate::Revnum,
) -> Result<(bool, bool, bool), crate::Error<'static>>;
fn dir_added(
&mut self,
path: &str,
rev: crate::Revnum,
copyfrom_path: Option<&str>,
copyfrom_revision: crate::Revnum,
) -> Result<(NotifyState, bool, bool, bool), crate::Error<'static>>;
fn dir_props_changed(
&mut self,
path: &str,
dir_was_added: bool,
prop_changes: &[PropChange],
) -> Result<(NotifyState, bool), crate::Error<'static>>;
fn dir_closed(
&mut self,
path: &str,
dir_was_added: bool,
) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>>;
}
pub struct DiffOptions {
pub depth: crate::Depth,
pub ignore_ancestry: bool,
pub show_copies_as_adds: bool,
pub use_git_diff_format: bool,
pub changelists: Vec<String>,
}
impl Default for DiffOptions {
fn default() -> Self {
Self {
depth: crate::Depth::Infinity,
ignore_ancestry: false,
show_copies_as_adds: false,
use_git_diff_format: false,
changelists: Vec::new(),
}
}
}
#[derive(Default)]
pub struct MergeOptions {
pub dry_run: bool,
pub diff3_cmd: Option<String>,
pub merge_options: Vec<String>,
}
pub struct RevertOptions {
pub depth: crate::Depth,
pub use_commit_times: bool,
pub changelists: Vec<String>,
pub clear_changelists: bool,
pub metadata_only: bool,
pub added_keep_local: bool,
}
impl Default for RevertOptions {
fn default() -> Self {
Self {
depth: crate::Depth::Empty,
use_commit_times: false,
changelists: Vec::new(),
clear_changelists: false,
metadata_only: false,
added_keep_local: true,
}
}
}
pub struct Context {
ptr: *mut svn_wc_context_t,
pool: apr::Pool<'static>,
_phantom: PhantomData<*mut ()>, }
impl Context {
pub fn close(&mut self) {
if !self.ptr.is_null() {
unsafe {
subversion_sys::svn_wc_context_destroy(self.ptr);
}
self.ptr = std::ptr::null_mut();
}
}
}
impl Drop for Context {
fn drop(&mut self) {
self.close();
}
}
pub mod adm;
#[allow(deprecated)]
pub use adm::Adm;
impl Context {
pub fn pool(&self) -> &apr::Pool<'_> {
&self.pool
}
pub fn as_ptr(&self) -> *const svn_wc_context_t {
self.ptr
}
pub fn as_mut_ptr(&mut self) -> *mut svn_wc_context_t {
self.ptr
}
pub fn new() -> Result<Self, crate::Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
})?;
Ok(Context {
ptr: ctx,
pool,
_phantom: PhantomData,
})
}
}
pub fn new_with_config(config: *mut std::ffi::c_void) -> Result<Self, crate::Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = subversion_sys::svn_wc_context_create(
&mut ctx,
config as *mut subversion_sys::svn_config_t,
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
);
svn_result(err)
})?;
Ok(Context {
ptr: ctx,
pool,
_phantom: PhantomData,
})
}
}
pub fn check_wc(&mut self, path: &str) -> Result<i32, crate::Error<'_>> {
let scratch_pool = apr::pool::Pool::new();
let path = crate::dirent::to_absolute_cstring(path)?;
let mut wc_format = 0;
let err = unsafe {
subversion_sys::svn_wc_check_wc2(
&mut wc_format,
self.ptr,
path.as_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(wc_format)
}
pub fn text_modified(&mut self, path: &str) -> Result<bool, crate::Error<'_>> {
let scratch_pool = apr::pool::Pool::new();
let path = crate::dirent::to_absolute_cstring(path)?;
let mut modified = 0;
let err = unsafe {
subversion_sys::svn_wc_text_modified_p2(
&mut modified,
self.ptr,
path.as_ptr(),
0,
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(modified != 0)
}
pub fn props_modified(&mut self, path: &str) -> Result<bool, crate::Error<'_>> {
let scratch_pool = apr::pool::Pool::new();
let path = crate::dirent::to_absolute_cstring(path)?;
let mut modified = 0;
let err = unsafe {
subversion_sys::svn_wc_props_modified_p2(
&mut modified,
self.ptr,
path.as_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(modified != 0)
}
pub fn conflicted(&mut self, path: &str) -> Result<(bool, bool, bool), crate::Error<'_>> {
let scratch_pool = apr::pool::Pool::new();
let path = crate::dirent::to_absolute_cstring(path)?;
let mut text_conflicted = 0;
let mut prop_conflicted = 0;
let mut tree_conflicted = 0;
let err = unsafe {
subversion_sys::svn_wc_conflicted_p3(
&mut text_conflicted,
&mut prop_conflicted,
&mut tree_conflicted,
self.ptr,
path.as_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok((
text_conflicted != 0,
prop_conflicted != 0,
tree_conflicted != 0,
))
}
pub fn ensure_adm(
&mut self,
local_abspath: &str,
url: &str,
repos_root_url: &str,
repos_uuid: &str,
revision: crate::Revnum,
depth: crate::Depth,
) -> Result<(), crate::Error<'_>> {
let scratch_pool = apr::pool::Pool::new();
let local_abspath = crate::dirent::to_absolute_cstring(local_abspath)?;
let url = crate::uri::canonicalize_uri(url)?;
let url = std::ffi::CString::new(url.as_str()).unwrap();
let repos_root_url = crate::uri::canonicalize_uri(repos_root_url)?;
let repos_root_url = std::ffi::CString::new(repos_root_url.as_str()).unwrap();
let repos_uuid = std::ffi::CString::new(repos_uuid).unwrap();
let err = unsafe {
subversion_sys::svn_wc_ensure_adm4(
self.ptr,
local_abspath.as_ptr(),
url.as_ptr(),
repos_root_url.as_ptr(),
repos_uuid.as_ptr(),
revision.0,
depth.into(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn locked(&mut self, path: &str) -> Result<(bool, bool), crate::Error<'_>> {
let path = crate::dirent::to_absolute_cstring(path)?;
let mut locked = 0;
let mut locked_here = 0;
let scratch_pool = apr::pool::Pool::new();
let err = unsafe {
subversion_sys::svn_wc_locked2(
&mut locked_here,
&mut locked,
self.ptr,
path.as_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok((locked != 0, locked_here != 0))
}
pub fn db_version(&self) -> Result<i32, crate::Error<'_>> {
Ok(0) }
pub fn upgrade(&mut self, local_abspath: &str) -> Result<(), crate::Error<'_>> {
let local_abspath_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let scratch_pool = apr::pool::Pool::new();
let err = unsafe {
subversion_sys::svn_wc_upgrade(
self.ptr,
local_abspath_cstr.as_ptr(),
None, std::ptr::null_mut(), None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn relocate(
&mut self,
wcroot_abspath: &str,
from: &str,
to: &str,
) -> Result<(), crate::Error<'_>> {
let wcroot_abspath_cstr = crate::dirent::to_absolute_cstring(wcroot_abspath)?;
let from_cstr = std::ffi::CString::new(from)?;
let to_cstr = std::ffi::CString::new(to)?;
let scratch_pool = apr::pool::Pool::new();
unsafe extern "C" fn default_validator(
_baton: *mut std::ffi::c_void,
_uuid: *const std::ffi::c_char,
_url: *const std::ffi::c_char,
_root_url: *const std::ffi::c_char,
_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
std::ptr::null_mut() }
let err = unsafe {
subversion_sys::svn_wc_relocate4(
self.ptr,
wcroot_abspath_cstr.as_ptr(),
from_cstr.as_ptr(),
to_cstr.as_ptr(),
Some(default_validator),
std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
pub fn add(
&mut self,
local_abspath: &str,
depth: crate::Depth,
copyfrom_url: Option<&str>,
copyfrom_rev: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
let local_abspath_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let copyfrom_url_cstr = copyfrom_url.map(std::ffi::CString::new).transpose()?;
let scratch_pool = apr::pool::Pool::new();
let err = unsafe {
subversion_sys::svn_wc_add4(
self.ptr,
local_abspath_cstr.as_ptr(),
depth.into(),
copyfrom_url_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
copyfrom_rev.map_or(-1, |r| r.into()),
None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
}
}
pub fn set_adm_dir(name: &str) -> Result<(), crate::Error<'_>> {
let scratch_pool = apr::pool::Pool::new();
let name = std::ffi::CString::new(name).unwrap();
let err =
unsafe { subversion_sys::svn_wc_set_adm_dir(name.as_ptr(), scratch_pool.as_mut_ptr()) };
Error::from_raw(err)?;
Ok(())
}
pub fn get_adm_dir() -> String {
let pool = apr::pool::Pool::new();
let name = unsafe { subversion_sys::svn_wc_get_adm_dir(pool.as_mut_ptr()) };
unsafe { std::ffi::CStr::from_ptr(name) }
.to_string_lossy()
.into_owned()
}
pub fn text_modified(
path: &std::path::Path,
force_comparison: bool,
) -> Result<bool, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut modified = 0;
with_tmp_pool(|pool| -> Result<(), crate::Error> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_text_modified_p2(
&mut modified,
ctx,
path_cstr.as_ptr(),
if force_comparison { 1 } else { 0 },
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})?;
Ok(modified != 0)
}
pub fn props_modified(path: &std::path::Path) -> Result<bool, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut modified = 0;
with_tmp_pool(|pool| -> Result<(), crate::Error> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_props_modified_p2(
&mut modified,
ctx,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})?;
Ok(modified != 0)
}
pub fn is_adm_dir(name: &str) -> bool {
let name_cstr = std::ffi::CString::new(name).unwrap();
let pool = apr::Pool::new();
let result =
unsafe { subversion_sys::svn_wc_is_adm_dir(name_cstr.as_ptr(), pool.as_mut_ptr()) };
result != 0
}
#[cfg(feature = "ra")]
pub fn crawl_revisions5(
wc_ctx: &mut Context,
local_abspath: &str,
reporter: &mut crate::ra::WrapReporter,
restore_files: bool,
depth: crate::Depth,
honor_depth_exclude: bool,
depth_compatibility_trick: bool,
use_commit_times: bool,
notify_func: Option<&dyn Fn(&Notify)>,
) -> Result<(), crate::Error<'static>> {
let local_abspath_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let notify_baton = notify_func
.map(|f| box_notify_baton_borrowed(f))
.unwrap_or(std::ptr::null_mut());
let result = with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_crawl_revisions5(
wc_ctx.as_mut_ptr(),
local_abspath_cstr.as_ptr(),
reporter.as_ptr(),
reporter.as_baton(),
if restore_files { 1 } else { 0 },
depth.into(),
if honor_depth_exclude { 1 } else { 0 },
if depth_compatibility_trick { 1 } else { 0 },
if use_commit_times { 1 } else { 0 },
None, std::ptr::null_mut(), if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
});
if !notify_baton.is_null() {
unsafe { drop_notify_baton_borrowed(notify_baton) };
}
result
}
pub fn get_update_editor4<'a>(
wc_ctx: &'a mut Context,
anchor_abspath: &str,
target_basename: &str,
options: UpdateEditorOptions,
) -> Result<(UpdateEditor<'a>, crate::Revnum), crate::Error<'static>> {
let anchor_abspath_cstr = crate::dirent::to_absolute_cstring(anchor_abspath)?;
let target_basename_cstr = std::ffi::CString::new(target_basename)?;
let diff3_cmd_cstr = options.diff3_cmd.map(std::ffi::CString::new).transpose()?;
let result_pool = apr::Pool::new();
let preserved_exts_cstrs: Vec<std::ffi::CString> = options
.preserved_exts
.iter()
.map(|&s| std::ffi::CString::new(s))
.collect::<Result<Vec<_>, _>>()?;
let preserved_exts_apr = if preserved_exts_cstrs.is_empty() {
std::ptr::null()
} else {
let mut arr = apr::tables::TypedArray::<*const i8>::new(
&result_pool,
preserved_exts_cstrs.len() as i32,
);
for cstr in &preserved_exts_cstrs {
arr.push(cstr.as_ptr());
}
unsafe { arr.as_ptr() }
};
let mut target_revision: subversion_sys::svn_revnum_t = 0;
let mut editor_ptr: *const subversion_sys::svn_delta_editor_t = std::ptr::null();
let mut edit_baton: *mut std::ffi::c_void = std::ptr::null_mut();
let has_fetch_dirents = options.fetch_dirents_func.is_some();
let fetch_dirents_baton = options
.fetch_dirents_func
.map(|f| box_fetch_dirents_baton(f))
.unwrap_or(std::ptr::null_mut());
let has_conflict = options.conflict_func.is_some();
let conflict_baton = options
.conflict_func
.map(|f| box_conflict_baton(f))
.unwrap_or(std::ptr::null_mut());
let has_external = options.external_func.is_some();
let external_baton = options
.external_func
.map(|f| box_external_baton(f))
.unwrap_or(std::ptr::null_mut());
let has_cancel = options.cancel_func.is_some();
let cancel_baton = options
.cancel_func
.map(box_cancel_baton)
.unwrap_or(std::ptr::null_mut());
let has_notify = options.notify_func.is_some();
let notify_baton = options
.notify_func
.map(|f| box_notify_baton(f))
.unwrap_or(std::ptr::null_mut());
let err = with_tmp_pool(|scratch_pool| unsafe {
svn_result(subversion_sys::svn_wc_get_update_editor4(
&mut editor_ptr,
&mut edit_baton,
&mut target_revision,
wc_ctx.as_mut_ptr(),
anchor_abspath_cstr.as_ptr(),
target_basename_cstr.as_ptr(),
if options.use_commit_times { 1 } else { 0 },
options.depth.into(),
if options.depth_is_sticky { 1 } else { 0 },
if options.allow_unver_obstructions {
1
} else {
0
},
if options.adds_as_modification { 1 } else { 0 },
if options.server_performs_filtering {
1
} else {
0
},
if options.clean_checkout { 1 } else { 0 },
diff3_cmd_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
preserved_exts_apr,
if has_fetch_dirents {
Some(wrap_fetch_dirents_func)
} else {
None
},
fetch_dirents_baton,
if has_conflict {
Some(wrap_conflict_func)
} else {
None
},
conflict_baton,
if has_external {
Some(wrap_external_func)
} else {
None
},
external_baton,
if has_cancel {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
if has_notify {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
))
});
err?;
let mut batons = Vec::new();
if !fetch_dirents_baton.is_null() {
batons.push((fetch_dirents_baton, drop_fetch_dirents_baton as DropperFn));
}
if !conflict_baton.is_null() {
batons.push((conflict_baton, drop_conflict_baton as DropperFn));
}
if !external_baton.is_null() {
batons.push((external_baton, drop_external_baton as DropperFn));
}
if !cancel_baton.is_null() {
batons.push((cancel_baton, drop_cancel_baton as DropperFn));
}
if !notify_baton.is_null() {
batons.push((notify_baton, drop_notify_baton as DropperFn));
}
let editor = UpdateEditor {
editor: editor_ptr,
edit_baton,
_pool: result_pool,
target_revision: crate::Revnum::from_raw(target_revision).unwrap_or_default(),
callback_batons: batons,
_marker: std::marker::PhantomData,
};
Ok((
editor,
crate::Revnum::from_raw(target_revision).unwrap_or_default(),
))
}
type DropperFn = unsafe fn(*mut std::ffi::c_void);
#[derive(Default)]
pub struct UpdateEditorOptions<'a> {
pub use_commit_times: bool,
pub depth: crate::Depth,
pub depth_is_sticky: bool,
pub allow_unver_obstructions: bool,
pub adds_as_modification: bool,
pub server_performs_filtering: bool,
pub clean_checkout: bool,
pub diff3_cmd: Option<&'a str>,
pub preserved_exts: Vec<&'a str>,
pub fetch_dirents_func: Option<
Box<
dyn Fn(
&str,
&str,
)
-> Result<std::collections::HashMap<String, crate::DirEntry>, Error<'static>>,
>,
>,
pub conflict_func: Option<
Box<
dyn Fn(
&crate::conflict::ConflictDescription,
) -> Result<crate::conflict::ConflictResult, Error<'static>>,
>,
>,
pub external_func: Option<
Box<dyn Fn(&str, Option<&str>, Option<&str>, crate::Depth) -> Result<(), Error<'static>>>,
>,
pub cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
pub notify_func: Option<Box<dyn Fn(&Notify)>>,
}
impl<'a> UpdateEditorOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_use_commit_times(mut self, use_commit_times: bool) -> Self {
self.use_commit_times = use_commit_times;
self
}
pub fn with_depth(mut self, depth: crate::Depth) -> Self {
self.depth = depth;
self
}
pub fn with_depth_is_sticky(mut self, sticky: bool) -> Self {
self.depth_is_sticky = sticky;
self
}
pub fn with_allow_unver_obstructions(mut self, allow: bool) -> Self {
self.allow_unver_obstructions = allow;
self
}
pub fn with_adds_as_modification(mut self, adds_as_mod: bool) -> Self {
self.adds_as_modification = adds_as_mod;
self
}
pub fn with_diff3_cmd(mut self, cmd: &'a str) -> Self {
self.diff3_cmd = Some(cmd);
self
}
pub fn with_preserved_exts(mut self, exts: Vec<&'a str>) -> Self {
self.preserved_exts = exts;
self
}
}
#[derive(Default)]
pub struct SwitchEditorOptions<'a> {
pub use_commit_times: bool,
pub depth: crate::Depth,
pub depth_is_sticky: bool,
pub allow_unver_obstructions: bool,
pub server_performs_filtering: bool,
pub diff3_cmd: Option<&'a str>,
pub preserved_exts: Vec<&'a str>,
pub fetch_dirents_func: Option<
Box<
dyn Fn(
&str,
&str,
)
-> Result<std::collections::HashMap<String, crate::DirEntry>, Error<'static>>,
>,
>,
pub conflict_func: Option<
Box<
dyn Fn(
&crate::conflict::ConflictDescription,
) -> Result<crate::conflict::ConflictResult, Error<'static>>,
>,
>,
pub external_func: Option<
Box<dyn Fn(&str, Option<&str>, Option<&str>, crate::Depth) -> Result<(), Error<'static>>>,
>,
pub cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
pub notify_func: Option<Box<dyn Fn(&Notify)>>,
}
impl<'a> SwitchEditorOptions<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn with_use_commit_times(mut self, use_commit_times: bool) -> Self {
self.use_commit_times = use_commit_times;
self
}
pub fn with_depth(mut self, depth: crate::Depth) -> Self {
self.depth = depth;
self
}
pub fn with_depth_is_sticky(mut self, sticky: bool) -> Self {
self.depth_is_sticky = sticky;
self
}
pub fn with_allow_unver_obstructions(mut self, allow: bool) -> Self {
self.allow_unver_obstructions = allow;
self
}
pub fn with_diff3_cmd(mut self, cmd: &'a str) -> Self {
self.diff3_cmd = Some(cmd);
self
}
pub fn with_preserved_exts(mut self, exts: Vec<&'a str>) -> Self {
self.preserved_exts = exts;
self
}
}
pub struct UpdateEditor<'a> {
editor: *const subversion_sys::svn_delta_editor_t,
edit_baton: *mut std::ffi::c_void,
_pool: apr::Pool<'static>,
target_revision: crate::Revnum,
callback_batons: Vec<(*mut std::ffi::c_void, DropperFn)>,
_marker: std::marker::PhantomData<&'a Context>,
}
impl Drop for UpdateEditor<'_> {
fn drop(&mut self) {
for (baton, dropper) in &self.callback_batons {
if !baton.is_null() {
unsafe {
dropper(*baton);
}
}
}
self.callback_batons.clear();
}
}
impl UpdateEditor<'_> {
pub fn target_revision(&self) -> crate::Revnum {
self.target_revision
}
pub fn into_wrap_editor(mut self) -> crate::delta::WrapEditor<'static> {
let editor = self.editor;
let baton = self.edit_baton;
let pool = std::mem::replace(&mut self._pool, apr::Pool::new());
let batons = std::mem::take(&mut self.callback_batons);
std::mem::forget(self);
crate::delta::WrapEditor {
editor,
baton,
_pool: apr::pool::PoolHandle::owned(pool),
callback_batons: batons,
}
}
}
impl crate::delta::Editor for UpdateEditor<'_> {
type RootEditor = crate::delta::WrapDirectoryEditor<'static>;
fn set_target_revision(
&mut self,
revision: Option<crate::Revnum>,
) -> Result<(), crate::Error<'_>> {
let scratch_pool = apr::Pool::new();
let err = unsafe {
((*self.editor).set_target_revision.unwrap())(
self.edit_baton,
revision.map_or(-1, |r| r.into()),
scratch_pool.as_mut_ptr(),
)
};
crate::Error::from_raw(err)?;
Ok(())
}
fn open_root(
&mut self,
base_revision: Option<crate::Revnum>,
) -> Result<crate::delta::WrapDirectoryEditor<'static>, crate::Error<'_>> {
let mut baton = std::ptr::null_mut();
let pool = apr::Pool::new();
let err = unsafe {
((*self.editor).open_root.unwrap())(
self.edit_baton,
base_revision.map_or(-1, |r| r.into()),
pool.as_mut_ptr(),
&mut baton,
)
};
crate::Error::from_raw(err)?;
Ok(crate::delta::WrapDirectoryEditor {
editor: self.editor,
baton,
_pool: apr::PoolHandle::owned(pool),
})
}
fn close(&mut self) -> Result<(), crate::Error<'_>> {
let scratch_pool = apr::Pool::new();
let err = unsafe {
((*self.editor).close_edit.unwrap())(self.edit_baton, scratch_pool.as_mut_ptr())
};
crate::Error::from_raw(err)?;
Ok(())
}
fn abort(&mut self) -> Result<(), crate::Error<'_>> {
let scratch_pool = apr::Pool::new();
let err = unsafe {
((*self.editor).abort_edit.unwrap())(self.edit_baton, scratch_pool.as_mut_ptr())
};
crate::Error::from_raw(err)?;
Ok(())
}
}
pub type DirEntries = std::collections::HashMap<String, crate::DirEntry>;
pub fn check_wc(path: &std::path::Path) -> Result<Option<i32>, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut wc_format = 0;
with_tmp_pool(|pool| -> Result<(), crate::Error> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_check_wc2(
&mut wc_format,
ctx,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})?;
if wc_format == 0 {
Ok(None)
} else {
Ok(Some(wc_format))
}
}
pub fn ensure_adm(
path: &std::path::Path,
uuid: &str,
url: &str,
repos_root: &str,
revision: i64,
) -> Result<(), crate::Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let uuid_cstr = std::ffi::CString::new(uuid).unwrap();
let url = crate::uri::canonicalize_uri(url)?;
let url_cstr = std::ffi::CString::new(url.as_str()).unwrap();
let repos_root = crate::uri::canonicalize_uri(repos_root)?;
let repos_root_cstr = std::ffi::CString::new(repos_root.as_str()).unwrap();
with_tmp_pool(|pool| -> Result<(), crate::Error> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_ensure_adm4(
ctx,
path_cstr.as_ptr(),
url_cstr.as_ptr(),
repos_root_cstr.as_ptr(),
uuid_cstr.as_ptr(),
revision as subversion_sys::svn_revnum_t,
subversion_sys::svn_depth_t_svn_depth_infinity,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})
}
pub fn is_normal_prop(name: &str) -> bool {
let name_cstr = std::ffi::CString::new(name).unwrap();
unsafe { subversion_sys::svn_wc_is_normal_prop(name_cstr.as_ptr()) != 0 }
}
pub fn is_entry_prop(name: &str) -> bool {
let name_cstr = std::ffi::CString::new(name).unwrap();
unsafe { subversion_sys::svn_wc_is_entry_prop(name_cstr.as_ptr()) != 0 }
}
pub fn is_wc_prop(name: &str) -> bool {
let name_cstr = std::ffi::CString::new(name).unwrap();
unsafe { subversion_sys::svn_wc_is_wc_prop(name_cstr.as_ptr()) != 0 }
}
pub fn match_ignore_list(path: &str, patterns: &[&str]) -> Result<bool, crate::Error<'static>> {
let path_cstr = std::ffi::CString::new(path)?;
with_tmp_pool(|pool| {
let pattern_cstrs: Vec<std::ffi::CString> = patterns
.iter()
.map(|p| std::ffi::CString::new(*p))
.collect::<Result<Vec<_>, _>>()?;
let mut patterns_array =
apr::tables::TypedArray::<*const i8>::new(pool, patterns.len() as i32);
for pattern_cstr in &pattern_cstrs {
patterns_array.push(pattern_cstr.as_ptr());
}
let matched = unsafe {
subversion_sys::svn_wc_match_ignore_list(
path_cstr.as_ptr(),
patterns_array.as_ptr(),
pool.as_mut_ptr(),
)
};
Ok(matched != 0)
})
}
pub fn get_actual_target(path: &std::path::Path) -> Result<(String, String), crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut anchor: *const i8 = std::ptr::null();
let mut target: *const i8 = std::ptr::null();
with_tmp_pool(|pool| -> Result<(), crate::Error> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_get_actual_target2(
&mut anchor,
&mut target,
ctx,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})?;
let anchor_str = if anchor.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(anchor) }
.to_string_lossy()
.into_owned()
};
let target_str = if target.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(target) }
.to_string_lossy()
.into_owned()
};
Ok((anchor_str, target_str))
}
pub fn get_pristine_contents(
path: &std::path::Path,
) -> Result<Option<crate::io::Stream>, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut contents: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();
let result_pool = apr::Pool::new();
with_tmp_pool(|scratch_pool| -> Result<(), crate::Error> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|ctx_scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
scratch_pool.as_mut_ptr(), ctx_scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_get_pristine_contents2(
&mut contents,
ctx,
path_cstr.as_ptr(),
result_pool.as_mut_ptr(), scratch_pool.as_mut_ptr(), )
};
Error::from_raw(err)?;
Ok(())
})?;
if contents.is_null() {
Ok(None)
} else {
Ok(Some(unsafe {
crate::io::Stream::from_ptr_and_pool(contents, result_pool)
}))
}
}
pub fn get_pristine_copy_path(
path: &std::path::Path,
) -> Result<std::path::PathBuf, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut pristine_path: *const i8 = std::ptr::null();
let pristine_path_str = with_tmp_pool(|pool| -> Result<String, crate::Error> {
let err = unsafe {
subversion_sys::svn_wc_get_pristine_copy_path(
path_cstr.as_ptr(),
&mut pristine_path,
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let result = if pristine_path.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(pristine_path) }
.to_string_lossy()
.into_owned()
};
Ok(result)
})?;
Ok(std::path::PathBuf::from(pristine_path_str))
}
impl Context {
pub fn get_actual_target(&mut self, path: &str) -> Result<(String, String), crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut anchor: *const i8 = std::ptr::null();
let mut target: *const i8 = std::ptr::null();
let pool = apr::Pool::new();
let err = unsafe {
subversion_sys::svn_wc_get_actual_target2(
&mut anchor,
&mut target,
self.ptr,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let anchor_str = if anchor.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(anchor) }
.to_string_lossy()
.into_owned()
};
let target_str = if target.is_null() {
String::new()
} else {
unsafe { std::ffi::CStr::from_ptr(target) }
.to_string_lossy()
.into_owned()
};
Ok((anchor_str, target_str))
}
pub fn get_pristine_contents(
&mut self,
path: &str,
) -> Result<Option<crate::io::Stream>, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut contents: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();
let pool = apr::Pool::new();
let err = unsafe {
subversion_sys::svn_wc_get_pristine_contents2(
&mut contents,
self.ptr,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if contents.is_null() {
Ok(None)
} else {
Ok(Some(unsafe {
crate::io::Stream::from_ptr_and_pool(contents, pool)
}))
}
}
pub fn get_pristine_props(
&mut self,
path: &str,
) -> Result<Option<std::collections::HashMap<String, Vec<u8>>>, crate::Error<'_>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let mut props: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
let pool = apr::Pool::new();
let err = unsafe {
subversion_sys::svn_wc_get_pristine_props(
&mut props,
self.ptr,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if props.is_null() {
return Ok(None);
}
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
Ok(Some(prop_hash.to_hashmap()))
}
pub fn walk_status<F>(
&mut self,
local_abspath: &std::path::Path,
depth: crate::Depth,
get_all: bool,
no_ignore: bool,
ignore_text_mods: bool,
ignore_patterns: Option<&[&str]>,
status_func: F,
) -> Result<(), Error<'static>>
where
F: FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>,
{
let pool = apr::Pool::new();
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let pattern_cstrs: Vec<std::ffi::CString> = ignore_patterns
.unwrap_or(&[])
.iter()
.map(|p| std::ffi::CString::new(*p).expect("pattern valid UTF-8"))
.collect();
let ignore_patterns_ptr = if let Some(patterns) = ignore_patterns {
let mut arr = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
&pool,
patterns.len() as i32,
);
for cstr in &pattern_cstrs {
arr.push(cstr.as_ptr());
}
unsafe { arr.as_ptr() }
} else {
std::ptr::null_mut()
};
unsafe extern "C" fn status_callback(
baton: *mut std::ffi::c_void,
local_abspath: *const std::os::raw::c_char,
status: *const subversion_sys::svn_wc_status3_t,
scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
let callback = unsafe {
&mut *(baton
as *mut Box<dyn FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>>)
};
let local_path =
unsafe { subversion_sys::svn_dirent_local_style(local_abspath, scratch_pool) };
let path = unsafe {
std::ffi::CStr::from_ptr(local_path)
.to_string_lossy()
.into_owned()
};
let status = Status {
ptr: status,
_pool: unsafe { apr::pool::PoolHandle::from_borrowed_raw(scratch_pool) },
};
match callback(&path, &status) {
Ok(()) => std::ptr::null_mut(),
Err(e) => unsafe { e.into_raw() },
}
}
let boxed_callback: Box<Box<dyn FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>>> =
Box::new(Box::new(status_func));
let baton = Box::into_raw(boxed_callback) as *mut std::ffi::c_void;
unsafe {
let err = subversion_sys::svn_wc_walk_status(
self.ptr,
path_cstr.as_ptr(),
depth.into(),
get_all as i32,
no_ignore as i32,
ignore_text_mods as i32,
ignore_patterns_ptr,
Some(status_callback),
baton,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
let _ = Box::from_raw(
baton as *mut Box<dyn FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>>,
);
Error::from_raw(err)
}
}
pub fn queue_committed(
&mut self,
local_abspath: &std::path::Path,
recurse: bool,
is_committed: bool,
committed_queue: &mut CommittedQueue,
wcprop_changes: Option<&[PropChange]>,
remove_lock: bool,
remove_changelist: bool,
sha1_checksum: Option<&crate::Checksum>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let wcprop_changes_ptr = if let Some(changes) = wcprop_changes {
let prop_name_cstrs: Vec<std::ffi::CString> = changes
.iter()
.map(|c| std::ffi::CString::new(c.name.as_str()).expect("prop name valid UTF-8"))
.collect();
let mut arr = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(
&pool,
changes.len() as i32,
);
for (change, name_cstr) in changes.iter().zip(prop_name_cstrs.iter()) {
arr.push(subversion_sys::svn_prop_t {
name: name_cstr.as_ptr(),
value: if let Some(v) = &change.value {
crate::svn_string_helpers::svn_string_ncreate(v, &pool)
} else {
std::ptr::null()
},
});
}
unsafe { arr.as_ptr() }
} else {
std::ptr::null()
};
let sha1_ptr = sha1_checksum.map(|c| c.ptr).unwrap_or(std::ptr::null());
unsafe {
let err = subversion_sys::svn_wc_queue_committed4(
committed_queue.as_mut_ptr(),
self.ptr,
path_cstr.as_ptr(),
recurse as i32,
is_committed as i32,
wcprop_changes_ptr,
remove_lock as i32,
remove_changelist as i32,
sha1_ptr,
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn process_committed_queue(
&mut self,
committed_queue: &mut CommittedQueue,
new_revnum: crate::Revnum,
rev_date: Option<&str>,
rev_author: Option<&str>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let rev_date_cstr = rev_date.map(std::ffi::CString::new).transpose()?;
let rev_author_cstr = rev_author.map(std::ffi::CString::new).transpose()?;
unsafe {
let err = subversion_sys::svn_wc_process_committed_queue2(
committed_queue.as_mut_ptr(),
self.ptr,
new_revnum.0,
rev_date_cstr
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
rev_author_cstr
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn add_lock(
&mut self,
local_abspath: &std::path::Path,
lock: &Lock,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
unsafe {
let err = subversion_sys::svn_wc_add_lock2(
self.ptr,
path_cstr.as_ptr(),
lock.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn remove_lock(&mut self, local_abspath: &std::path::Path) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
unsafe {
let err = subversion_sys::svn_wc_remove_lock2(
self.ptr,
path_cstr.as_ptr(),
pool.as_mut_ptr(),
);
Error::from_raw(err)
}
}
pub fn crop_tree(
&mut self,
local_abspath: &std::path::Path,
depth: crate::Depth,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let cancel_baton = cancel_func
.map(box_cancel_baton_borrowed)
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
subversion_sys::svn_wc_crop_tree2(
self.ptr,
path_cstr.as_ptr(),
depth.into(),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
)
};
if !cancel_baton.is_null() {
unsafe { drop_cancel_baton_borrowed(cancel_baton) };
}
Error::from_raw(ret)
}
pub fn resolved_conflict(
&mut self,
local_abspath: &std::path::Path,
depth: crate::Depth,
resolve_text: bool,
resolve_property: Option<&str>,
resolve_tree: bool,
conflict_choice: ConflictChoice,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
) -> Result<(), Error<'static>> {
let pool = apr::Pool::new();
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let prop_cstr = resolve_property.map(|p| std::ffi::CString::new(p).unwrap());
let prop_ptr = prop_cstr.as_ref().map_or(std::ptr::null(), |p| p.as_ptr());
let cancel_baton = cancel_func
.map(box_cancel_baton_borrowed)
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
subversion_sys::svn_wc_resolved_conflict5(
self.ptr,
path_cstr.as_ptr(),
depth.into(),
resolve_text.into(),
prop_ptr,
resolve_tree.into(),
conflict_choice.into(),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
)
};
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
Error::from_raw(ret)
}
pub fn add_from_disk(
&mut self,
local_abspath: &std::path::Path,
props: Option<&std::collections::HashMap<String, Vec<u8>>>,
skip_checks: bool,
notify_func: Option<&dyn Fn(&Notify)>,
) -> Result<(), Error<'static>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let pool = apr::Pool::new();
let props_hash = if let Some(props) = props {
let mut hash = apr::hash::Hash::new(&pool);
for (key, value) in props {
let svn_str = crate::svn_string_helpers::svn_string_ncreate(value, &pool);
unsafe {
hash.insert(key.as_bytes(), svn_str as *mut std::ffi::c_void);
}
}
unsafe { hash.as_mut_ptr() }
} else {
std::ptr::null_mut()
};
let notify_baton = notify_func
.map(|f| box_notify_baton_borrowed(f))
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
subversion_sys::svn_wc_add_from_disk3(
self.ptr,
path_cstr.as_ptr(),
props_hash,
skip_checks as i32,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
pool.as_mut_ptr(),
)
};
if !notify_baton.is_null() {
unsafe { drop_notify_baton_borrowed(notify_baton) };
}
Error::from_raw(ret)
}
pub fn add_repos_file(
&mut self,
local_abspath: &std::path::Path,
new_base_contents: &mut crate::io::Stream,
new_contents: Option<&mut crate::io::Stream>,
new_base_props: &std::collections::HashMap<String, Vec<u8>>,
new_props: Option<&std::collections::HashMap<String, Vec<u8>>>,
copyfrom_url: Option<&str>,
copyfrom_rev: crate::Revnum,
cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let copyfrom_url_cstr = copyfrom_url
.map(|u| std::ffi::CString::new(u).expect("copyfrom_url must be valid UTF-8"));
let scratch_pool = apr::Pool::new();
let build_props_hash = |props: &std::collections::HashMap<String, Vec<u8>>,
pool: &apr::Pool|
-> *mut apr_sys::apr_hash_t {
let mut hash = apr::hash::Hash::new(pool);
for (name, value) in props {
let svn_str = crate::svn_string_helpers::svn_string_ncreate(value, pool);
unsafe {
hash.insert(name.as_bytes(), svn_str as *mut std::ffi::c_void);
}
}
unsafe { hash.as_mut_ptr() }
};
let base_props_ptr = build_props_hash(new_base_props, &scratch_pool);
let props_ptr: *mut apr_sys::apr_hash_t = match new_props {
Some(p) => build_props_hash(p, &scratch_pool),
None => std::ptr::null_mut(),
};
let has_cancel = cancel_func.is_some();
let cancel_baton = cancel_func
.map(box_cancel_baton)
.unwrap_or(std::ptr::null_mut());
let err = unsafe {
let e = subversion_sys::svn_wc_add_repos_file4(
self.ptr,
path_cstr.as_ptr(),
new_base_contents.as_mut_ptr(),
new_contents
.map(|s| s.as_mut_ptr())
.unwrap_or(std::ptr::null_mut()),
base_props_ptr,
props_ptr,
copyfrom_url_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
copyfrom_rev.0,
if has_cancel {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
scratch_pool.as_mut_ptr(),
);
if has_cancel && !cancel_baton.is_null() {
drop_cancel_baton(cancel_baton);
}
e
};
Error::from_raw(err)
}
pub fn move_path(
&mut self,
src_abspath: &std::path::Path,
dst_abspath: &std::path::Path,
metadata_only: bool,
_allow_mixed_revisions: bool,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
notify_func: Option<&dyn Fn(&Notify)>,
) -> Result<(), Error<'static>> {
let src = src_abspath.to_str().unwrap();
let src_cstr = crate::dirent::to_absolute_cstring(src)?;
let dst = dst_abspath.to_str().unwrap();
let dst_cstr = crate::dirent::to_absolute_cstring(dst)?;
let pool = apr::Pool::new();
let cancel_baton = cancel_func
.map(box_cancel_baton_borrowed)
.unwrap_or(std::ptr::null_mut());
let notify_baton = notify_func
.map(|f| box_notify_baton_borrowed(f))
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
subversion_sys::svn_wc_move(
self.ptr,
src_cstr.as_ptr(),
dst_cstr.as_ptr(),
metadata_only.into(),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
pool.as_mut_ptr(),
)
};
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
if !notify_baton.is_null() {
unsafe { drop_notify_baton_borrowed(notify_baton) };
}
Error::from_raw(ret)
}
pub fn get_switch_editor<'s>(
&'s mut self,
anchor_abspath: &str,
target_basename: &str,
switch_url: &str,
options: SwitchEditorOptions,
) -> Result<(crate::delta::WrapEditor<'s>, crate::Revnum), crate::Error<'s>> {
let anchor_abspath_cstr = crate::dirent::to_absolute_cstring(anchor_abspath)?;
let target_basename_cstr = std::ffi::CString::new(target_basename)?;
let switch_url_cstr = std::ffi::CString::new(switch_url)?;
let diff3_cmd_cstr = options.diff3_cmd.map(std::ffi::CString::new).transpose()?;
let result_pool = apr::Pool::new();
let preserved_exts_cstrs: Vec<std::ffi::CString> = options
.preserved_exts
.iter()
.map(|&s| std::ffi::CString::new(s))
.collect::<Result<Vec<_>, _>>()?;
let preserved_exts_apr = if preserved_exts_cstrs.is_empty() {
std::ptr::null()
} else {
let mut arr = apr::tables::TypedArray::<*const i8>::new(
&result_pool,
preserved_exts_cstrs.len() as i32,
);
for cstr in &preserved_exts_cstrs {
arr.push(cstr.as_ptr());
}
unsafe { arr.as_ptr() }
};
let mut target_revision: subversion_sys::svn_revnum_t = 0;
let mut editor_ptr: *const subversion_sys::svn_delta_editor_t = std::ptr::null();
let mut edit_baton: *mut std::ffi::c_void = std::ptr::null_mut();
let has_fetch_dirents = options.fetch_dirents_func.is_some();
let fetch_dirents_baton = options
.fetch_dirents_func
.map(|f| box_fetch_dirents_baton(f))
.unwrap_or(std::ptr::null_mut());
let has_conflict = options.conflict_func.is_some();
let conflict_baton = options
.conflict_func
.map(|f| box_conflict_baton(f))
.unwrap_or(std::ptr::null_mut());
let has_external = options.external_func.is_some();
let external_baton = options
.external_func
.map(|f| box_external_baton(f))
.unwrap_or(std::ptr::null_mut());
let has_cancel = options.cancel_func.is_some();
let cancel_baton = options
.cancel_func
.map(box_cancel_baton)
.unwrap_or(std::ptr::null_mut());
let has_notify = options.notify_func.is_some();
let notify_baton = options
.notify_func
.map(|f| box_notify_baton(f))
.unwrap_or(std::ptr::null_mut());
let err = with_tmp_pool(|scratch_pool| unsafe {
svn_result(subversion_sys::svn_wc_get_switch_editor4(
&mut editor_ptr,
&mut edit_baton,
&mut target_revision,
self.ptr,
anchor_abspath_cstr.as_ptr(),
target_basename_cstr.as_ptr(),
switch_url_cstr.as_ptr(),
if options.use_commit_times { 1 } else { 0 },
options.depth.into(),
if options.depth_is_sticky { 1 } else { 0 },
if options.allow_unver_obstructions {
1
} else {
0
},
if options.server_performs_filtering {
1
} else {
0
},
diff3_cmd_cstr
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr()),
preserved_exts_apr,
if has_fetch_dirents {
Some(wrap_fetch_dirents_func)
} else {
None
},
fetch_dirents_baton,
if has_conflict {
Some(wrap_conflict_func)
} else {
None
},
conflict_baton,
if has_external {
Some(wrap_external_func)
} else {
None
},
external_baton,
if has_cancel {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
if has_notify {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
))
});
err?;
let mut batons = Vec::new();
if !fetch_dirents_baton.is_null() {
batons.push((
fetch_dirents_baton,
drop_fetch_dirents_baton as crate::delta::DropperFn,
));
}
if !conflict_baton.is_null() {
batons.push((
conflict_baton,
drop_conflict_baton as crate::delta::DropperFn,
));
}
if !external_baton.is_null() {
batons.push((
external_baton,
drop_external_baton as crate::delta::DropperFn,
));
}
if !cancel_baton.is_null() {
batons.push((cancel_baton, drop_cancel_baton as crate::delta::DropperFn));
}
if !notify_baton.is_null() {
batons.push((notify_baton, drop_notify_baton as crate::delta::DropperFn));
}
let editor = crate::delta::WrapEditor {
editor: editor_ptr,
baton: edit_baton,
_pool: apr::PoolHandle::owned(result_pool),
callback_batons: batons,
};
Ok((
editor,
crate::Revnum::from_raw(target_revision).unwrap_or_default(),
))
}
pub fn get_diff_editor<'s>(
&'s mut self,
anchor_abspath: &str,
target_abspath: &str,
callbacks: &'s mut dyn DiffCallbacks,
use_text_base: bool,
depth: crate::Depth,
ignore_ancestry: bool,
show_copies_as_adds: bool,
use_git_diff_format: bool,
) -> Result<crate::delta::WrapEditor<'s>, crate::Error<'s>> {
let anchor_abspath_cstr = crate::dirent::to_absolute_cstring(anchor_abspath)?;
let target_abspath_cstr = crate::dirent::to_absolute_cstring(target_abspath)?;
let result_pool = apr::Pool::new();
let mut editor_ptr: *const subversion_sys::svn_delta_editor_t = std::ptr::null();
let mut edit_baton: *mut std::ffi::c_void = std::ptr::null_mut();
let c_callbacks = Box::new(make_diff_callbacks4());
let c_callbacks_ptr = &*c_callbacks as *const subversion_sys::svn_wc_diff_callbacks4_t;
let cb_baton: Box<*mut dyn DiffCallbacks> = Box::new(callbacks as *mut dyn DiffCallbacks);
let cb_baton_ptr = &*cb_baton as *const *mut dyn DiffCallbacks as *mut std::ffi::c_void;
let err = with_tmp_pool(|scratch_pool| unsafe {
svn_result(subversion_sys::svn_wc_get_diff_editor6(
&mut editor_ptr,
&mut edit_baton,
self.ptr,
anchor_abspath_cstr.as_ptr(),
target_abspath_cstr.as_ptr(),
depth.into(),
if ignore_ancestry { 1 } else { 0 },
if show_copies_as_adds { 1 } else { 0 },
if use_git_diff_format { 1 } else { 0 },
if use_text_base { 1 } else { 0 },
0, 0, std::ptr::null(), c_callbacks_ptr,
cb_baton_ptr,
None, std::ptr::null_mut(), result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
))
});
err?;
unsafe fn drop_callbacks(ptr: *mut std::ffi::c_void) {
let _ = Box::from_raw(ptr as *mut subversion_sys::svn_wc_diff_callbacks4_t);
}
unsafe fn drop_baton(ptr: *mut std::ffi::c_void) {
let _ = Box::from_raw(ptr as *mut *mut dyn DiffCallbacks);
}
let batons: Vec<(*mut std::ffi::c_void, crate::delta::DropperFn)> = vec![
(
Box::into_raw(c_callbacks) as *mut std::ffi::c_void,
drop_callbacks as crate::delta::DropperFn,
),
(
Box::into_raw(cb_baton) as *mut std::ffi::c_void,
drop_baton as crate::delta::DropperFn,
),
];
let editor = crate::delta::WrapEditor {
editor: editor_ptr,
baton: edit_baton,
_pool: apr::PoolHandle::owned(result_pool),
callback_batons: batons,
};
Ok(editor)
}
pub fn delete(
&mut self,
local_abspath: &std::path::Path,
keep_local: bool,
delete_unversioned_target: bool,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
notify_func: Option<&dyn Fn(&Notify)>,
) -> Result<(), Error<'static>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let pool = apr::Pool::new();
let cancel_baton = cancel_func
.map(box_cancel_baton_borrowed)
.unwrap_or(std::ptr::null_mut());
let notify_baton = notify_func
.map(|f| box_notify_baton_borrowed(f))
.unwrap_or(std::ptr::null_mut());
let ret = unsafe {
subversion_sys::svn_wc_delete4(
self.ptr,
path_cstr.as_ptr(),
keep_local.into(),
delete_unversioned_target.into(),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
pool.as_mut_ptr(),
)
};
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
if !notify_baton.is_null() {
unsafe { drop_notify_baton_borrowed(notify_baton) };
}
Error::from_raw(ret)
}
pub fn prop_get(
&mut self,
local_abspath: &std::path::Path,
name: &str,
) -> Result<Option<Vec<u8>>, Error<'_>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let name_cstr = std::ffi::CString::new(name).unwrap();
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let mut value: *const subversion_sys::svn_string_t = std::ptr::null();
let err = unsafe {
subversion_sys::svn_wc_prop_get2(
&mut value,
self.ptr,
path_cstr.as_ptr(),
name_cstr.as_ptr(),
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
if value.is_null() {
Ok(None)
} else {
Ok(Some(Vec::from(unsafe {
std::slice::from_raw_parts((*value).data as *const u8, (*value).len)
})))
}
}
pub fn prop_set(
&mut self,
local_abspath: &std::path::Path,
name: &str,
value: Option<&[u8]>,
depth: crate::Depth,
skip_checks: bool,
changelist_filter: Option<&[&str]>,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
notify_func: Option<&dyn Fn(&Notify)>,
) -> Result<(), Error<'static>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let name_cstr = std::ffi::CString::new(name).unwrap();
let scratch_pool = apr::Pool::new();
let value_svn = value.map(|v| crate::string::BStr::from_bytes(v, &scratch_pool));
let value_ptr = value_svn
.as_ref()
.map(|v| v.as_ptr())
.unwrap_or(std::ptr::null());
let changelist_cstrings: Vec<_> = changelist_filter
.map(|lists| {
lists
.iter()
.map(|l| std::ffi::CString::new(*l).unwrap())
.collect()
})
.unwrap_or_default();
let changelist_array = if changelist_filter.is_some() {
let mut array = apr::tables::TypedArray::<*const i8>::new(
&scratch_pool,
changelist_cstrings.len() as i32,
);
for cstring in &changelist_cstrings {
array.push(cstring.as_ptr());
}
unsafe { array.as_ptr() }
} else {
std::ptr::null()
};
let cancel_baton = cancel_func
.map(box_cancel_baton_borrowed)
.unwrap_or(std::ptr::null_mut());
let notify_baton = notify_func
.map(|f| box_notify_baton_borrowed(f))
.unwrap_or(std::ptr::null_mut());
let err = unsafe {
subversion_sys::svn_wc_prop_set4(
self.ptr,
path_cstr.as_ptr(),
name_cstr.as_ptr(),
value_ptr,
depth.into(),
skip_checks.into(),
changelist_array,
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
scratch_pool.as_mut_ptr(),
)
};
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
))
};
}
if !notify_baton.is_null() {
unsafe { drop_notify_baton_borrowed(notify_baton) };
}
Error::from_raw(err)
}
pub fn prop_list(
&mut self,
local_abspath: &std::path::Path,
) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let mut props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_wc_prop_list2(
&mut props,
self.ptr,
path_cstr.as_ptr(),
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
Ok(prop_hash.to_hashmap())
}
pub fn get_prop_diffs(
&mut self,
local_abspath: &std::path::Path,
) -> Result<
(
Vec<PropChange>,
Option<std::collections::HashMap<String, Vec<u8>>>,
),
Error<'_>,
> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let mut propchanges: *mut apr::tables::apr_array_header_t = std::ptr::null_mut();
let mut original_props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
let err = unsafe {
subversion_sys::svn_wc_get_prop_diffs2(
&mut propchanges,
&mut original_props,
self.ptr,
path_cstr.as_ptr(),
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
let changes = if propchanges.is_null() {
Vec::new()
} else {
let array = unsafe {
apr::tables::TypedArray::<subversion_sys::svn_prop_t>::from_ptr(propchanges)
};
array
.iter()
.map(|prop| unsafe {
if prop.name.is_null() {
panic!("Encountered null prop.name in propchanges array");
}
let name = std::ffi::CStr::from_ptr(prop.name)
.to_str()
.expect("Property name is not valid UTF-8")
.to_owned();
let value = if prop.value.is_null() {
None
} else {
Some(crate::svn_string_helpers::to_vec(&*prop.value))
};
PropChange { name, value }
})
.collect()
};
let original = if original_props.is_null() {
None
} else {
let prop_hash = unsafe { crate::props::PropHash::from_ptr(original_props) };
Some(prop_hash.to_hashmap())
};
Ok((changes, original))
}
pub fn read_kind(
&mut self,
local_abspath: &std::path::Path,
show_deleted: bool,
show_hidden: bool,
) -> Result<crate::NodeKind, Error<'static>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let scratch_pool = apr::Pool::new();
let mut kind: subversion_sys::svn_node_kind_t =
subversion_sys::svn_node_kind_t_svn_node_none;
let err = unsafe {
subversion_sys::svn_wc_read_kind2(
&mut kind,
self.ptr,
path_cstr.as_ptr(),
show_deleted.into(),
show_hidden.into(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(kind.into())
}
pub fn is_wc_root(&mut self, local_abspath: &std::path::Path) -> Result<bool, Error<'static>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let scratch_pool = apr::Pool::new();
let mut wc_root: i32 = 0;
let err = unsafe {
subversion_sys::svn_wc_is_wc_root2(
&mut wc_root,
self.ptr,
path_cstr.as_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(wc_root != 0)
}
pub fn exclude(
&mut self,
local_abspath: &std::path::Path,
cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
notify_func: Option<&dyn Fn(&Notify)>,
) -> Result<(), Error<'static>> {
let path = local_abspath.to_str().unwrap();
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
let scratch_pool = apr::Pool::new();
let cancel_baton = cancel_func
.map(box_cancel_baton_borrowed)
.unwrap_or(std::ptr::null_mut());
let notify_baton = notify_func
.map(|f| box_notify_baton_borrowed(f))
.unwrap_or(std::ptr::null_mut());
let err = unsafe {
subversion_sys::svn_wc_exclude(
self.ptr,
path_cstr.as_ptr(),
if cancel_func.is_some() {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
if notify_func.is_some() {
Some(wrap_notify_func)
} else {
None
},
notify_baton,
scratch_pool.as_mut_ptr(),
)
};
if !cancel_baton.is_null() {
unsafe {
drop(Box::from_raw(
cancel_baton as *mut &dyn Fn() -> Result<(), Error<'static>>,
));
}
}
if !notify_baton.is_null() {
unsafe {
drop(Box::from_raw(notify_baton as *mut &dyn Fn(&Notify)));
}
}
Error::from_raw(err)
}
pub fn diff(
&mut self,
target_abspath: &std::path::Path,
options: &DiffOptions,
callbacks: &mut dyn DiffCallbacks,
) -> Result<(), Error<'static>> {
let target_cstr = crate::dirent::to_absolute_cstring(target_abspath)?;
let scratch_pool = apr::Pool::new();
let changelist_cstrs: Vec<std::ffi::CString> = options
.changelists
.iter()
.map(|cl| std::ffi::CString::new(cl.as_str()).expect("changelist is valid UTF-8"))
.collect();
let mut changelist_arr =
apr::tables::TypedArray::<*const i8>::new(&scratch_pool, changelist_cstrs.len() as i32);
for s in &changelist_cstrs {
changelist_arr.push(s.as_ptr());
}
let changelist_filter: *const apr_sys::apr_array_header_t =
if options.changelists.is_empty() {
std::ptr::null()
} else {
unsafe { changelist_arr.as_ptr() }
};
let c_callbacks = make_diff_callbacks4();
let mut cb_ref: &mut dyn DiffCallbacks = callbacks;
let baton = &mut cb_ref as *mut &mut dyn DiffCallbacks as *mut std::ffi::c_void;
with_tmp_pool(|scratch| unsafe {
svn_result(subversion_sys::svn_wc_diff6(
self.ptr,
target_cstr.as_ptr(),
&c_callbacks,
baton,
options.depth.into(),
options.ignore_ancestry as i32,
options.show_copies_as_adds as i32,
options.use_git_diff_format as i32,
changelist_filter,
None, std::ptr::null_mut(), scratch.as_mut_ptr(),
))
})
}
pub fn merge(
&mut self,
left_abspath: &std::path::Path,
right_abspath: &std::path::Path,
target_abspath: &std::path::Path,
left_label: Option<&str>,
right_label: Option<&str>,
target_label: Option<&str>,
prop_diff: &[PropChange],
options: &MergeOptions,
) -> Result<(MergeOutcome, NotifyState), Error<'static>> {
let left_cstr = crate::dirent::to_absolute_cstring(left_abspath)?;
let right_cstr = crate::dirent::to_absolute_cstring(right_abspath)?;
let target_cstr = crate::dirent::to_absolute_cstring(target_abspath)?;
let left_label_cstr =
left_label.map(|s| std::ffi::CString::new(s).expect("label must be valid UTF-8"));
let right_label_cstr =
right_label.map(|s| std::ffi::CString::new(s).expect("label must be valid UTF-8"));
let target_label_cstr =
target_label.map(|s| std::ffi::CString::new(s).expect("label must be valid UTF-8"));
let diff3_cstr = options
.diff3_cmd
.as_deref()
.map(|s| std::ffi::CString::new(s).expect("diff3_cmd must be valid UTF-8"));
let scratch_pool = apr::Pool::new();
let prop_name_cstrs: Vec<std::ffi::CString> = prop_diff
.iter()
.map(|c| std::ffi::CString::new(c.name.as_str()).expect("prop name valid UTF-8"))
.collect();
let mut prop_diff_typed = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(
&scratch_pool,
prop_diff.len() as i32,
);
for (change, name_cstr) in prop_diff.iter().zip(prop_name_cstrs.iter()) {
prop_diff_typed.push(subversion_sys::svn_prop_t {
name: name_cstr.as_ptr(),
value: if let Some(v) = &change.value {
crate::svn_string_helpers::svn_string_ncreate(v, &scratch_pool)
} else {
std::ptr::null()
},
});
}
let prop_diff_arr: *const apr_sys::apr_array_header_t = if prop_diff.is_empty() {
std::ptr::null()
} else {
unsafe { prop_diff_typed.as_ptr() }
};
let merge_opt_cstrs: Vec<std::ffi::CString> = options
.merge_options
.iter()
.map(|s| std::ffi::CString::new(s.as_str()).expect("merge option valid UTF-8"))
.collect();
let mut merge_opts_typed =
apr::tables::TypedArray::<*const i8>::new(&scratch_pool, merge_opt_cstrs.len() as i32);
for s in &merge_opt_cstrs {
merge_opts_typed.push(s.as_ptr());
}
let merge_opts_arr: *const apr_sys::apr_array_header_t = if options.merge_options.is_empty()
{
std::ptr::null()
} else {
unsafe { merge_opts_typed.as_ptr() }
};
let mut content_outcome = subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_no_merge;
let mut props_state =
subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable;
let err = unsafe {
svn_result(subversion_sys::svn_wc_merge5(
&mut content_outcome,
&mut props_state,
self.ptr,
left_cstr.as_ptr(),
right_cstr.as_ptr(),
target_cstr.as_ptr(),
left_label_cstr
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
right_label_cstr
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
target_label_cstr
.as_ref()
.map_or(std::ptr::null(), |s| s.as_ptr()),
std::ptr::null(), std::ptr::null(), options.dry_run as i32,
diff3_cstr.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
merge_opts_arr,
std::ptr::null_mut(), prop_diff_arr,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
))
};
err?;
Ok((content_outcome.into(), props_state.into()))
}
pub fn merge_props(
&mut self,
local_abspath: &std::path::Path,
baseprops: Option<&std::collections::HashMap<String, Vec<u8>>>,
propchanges: &[PropChange],
dry_run: bool,
conflict_func: Option<
Box<
dyn Fn(
&crate::conflict::ConflictDescription,
) -> Result<crate::conflict::ConflictResult, Error<'static>>,
>,
>,
cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
) -> Result<NotifyState, Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let scratch_pool = apr::Pool::new();
let baseprops_hash_ptr: *mut apr_sys::apr_hash_t = if let Some(props) = baseprops {
let mut hash = apr::hash::Hash::new(&scratch_pool);
let mut key_cstrs: Vec<std::ffi::CString> = Vec::with_capacity(props.len());
let mut svn_strings: Vec<*mut subversion_sys::svn_string_t> =
Vec::with_capacity(props.len());
for (name, value) in props {
key_cstrs
.push(std::ffi::CString::new(name.as_str()).expect("prop name valid UTF-8"));
svn_strings.push(crate::svn_string_helpers::svn_string_ncreate(
value,
&scratch_pool,
));
}
for (cstr, svn_str) in key_cstrs.iter().zip(svn_strings.iter()) {
unsafe {
hash.insert(cstr.as_bytes(), *svn_str as *mut std::ffi::c_void);
}
}
unsafe { hash.as_mut_ptr() }
} else {
std::ptr::null_mut()
};
let prop_name_cstrs: Vec<std::ffi::CString> = propchanges
.iter()
.map(|c| std::ffi::CString::new(c.name.as_str()).expect("prop name valid UTF-8"))
.collect();
let mut prop_changes_typed = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(
&scratch_pool,
propchanges.len() as i32,
);
for (change, name_cstr) in propchanges.iter().zip(prop_name_cstrs.iter()) {
prop_changes_typed.push(subversion_sys::svn_prop_t {
name: name_cstr.as_ptr(),
value: if let Some(v) = &change.value {
crate::svn_string_helpers::svn_string_ncreate(v, &scratch_pool)
} else {
std::ptr::null()
},
});
}
let propchanges_arr: *const apr_sys::apr_array_header_t = if propchanges.is_empty() {
std::ptr::null()
} else {
unsafe { prop_changes_typed.as_ptr() }
};
let has_conflict = conflict_func.is_some();
let conflict_baton = conflict_func
.map(box_conflict_baton)
.unwrap_or(std::ptr::null_mut());
let has_cancel = cancel_func.is_some();
let cancel_baton = cancel_func
.map(box_cancel_baton)
.unwrap_or(std::ptr::null_mut());
let mut state = subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable;
let err = unsafe {
let e = subversion_sys::svn_wc_merge_props3(
&mut state,
self.ptr,
path_cstr.as_ptr(),
std::ptr::null(), std::ptr::null(), baseprops_hash_ptr,
propchanges_arr,
dry_run as i32,
if has_conflict {
Some(wrap_conflict_func)
} else {
None
},
conflict_baton,
if has_cancel {
Some(crate::wrap_cancel_func)
} else {
None
},
cancel_baton,
scratch_pool.as_mut_ptr(),
);
if has_conflict && !conflict_baton.is_null() {
drop_conflict_baton(conflict_baton);
}
if has_cancel && !cancel_baton.is_null() {
drop_cancel_baton(cancel_baton);
}
e
};
Error::from_raw(err)?;
Ok(state.into())
}
pub fn revert(
&mut self,
local_abspath: &std::path::Path,
options: &RevertOptions,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let scratch_pool = apr::Pool::new();
let changelist_cstrs: Vec<std::ffi::CString> = options
.changelists
.iter()
.map(|cl| std::ffi::CString::new(cl.as_str()).expect("changelist is valid UTF-8"))
.collect();
let mut changelist_arr =
apr::tables::TypedArray::<*const i8>::new(&scratch_pool, changelist_cstrs.len() as i32);
for s in &changelist_cstrs {
changelist_arr.push(s.as_ptr());
}
let changelist_filter: *const apr_sys::apr_array_header_t =
if options.changelists.is_empty() {
std::ptr::null()
} else {
unsafe { changelist_arr.as_ptr() }
};
svn_result(unsafe {
subversion_sys::svn_wc_revert6(
self.ptr,
path_cstr.as_ptr(),
options.depth.into(),
options.use_commit_times as i32,
changelist_filter,
options.clear_changelists as i32,
options.metadata_only as i32,
options.added_keep_local as i32,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
})
}
pub fn copy(
&mut self,
src_abspath: &std::path::Path,
dst_abspath: &std::path::Path,
metadata_only: bool,
) -> Result<(), Error<'static>> {
let src_cstr = crate::dirent::to_absolute_cstring(src_abspath)?;
let dst_cstr = crate::dirent::to_absolute_cstring(dst_abspath)?;
with_tmp_pool(|scratch| {
svn_result(unsafe {
subversion_sys::svn_wc_copy3(
self.ptr,
src_cstr.as_ptr(),
dst_cstr.as_ptr(),
metadata_only as i32,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch.as_mut_ptr(),
)
})
})
}
pub fn set_changelist(
&mut self,
local_abspath: &std::path::Path,
changelist: Option<&str>,
depth: crate::Depth,
changelist_filter: &[String],
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let cl_cstr = changelist
.map(|s| std::ffi::CString::new(s).expect("changelist name must be valid UTF-8"));
let scratch_pool = apr::Pool::new();
let filter_cstrs: Vec<std::ffi::CString> = changelist_filter
.iter()
.map(|s| std::ffi::CString::new(s.as_str()).expect("filter name must be valid UTF-8"))
.collect();
let mut filter_arr =
apr::tables::TypedArray::<*const i8>::new(&scratch_pool, filter_cstrs.len() as i32);
for s in &filter_cstrs {
filter_arr.push(s.as_ptr());
}
let filter_ptr: *const apr_sys::apr_array_header_t = if changelist_filter.is_empty() {
std::ptr::null()
} else {
unsafe { filter_arr.as_ptr() }
};
svn_result(unsafe {
subversion_sys::svn_wc_set_changelist2(
self.ptr,
path_cstr.as_ptr(),
cl_cstr.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
depth.into(),
filter_ptr,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
})
}
pub fn get_changelists(
&mut self,
local_abspath: &std::path::Path,
depth: crate::Depth,
changelist_filter: &[String],
mut callback: impl FnMut(&str, Option<&str>) -> Result<(), Error<'static>>,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let scratch_pool = apr::Pool::new();
let filter_cstrs: Vec<std::ffi::CString> = changelist_filter
.iter()
.map(|s| std::ffi::CString::new(s.as_str()).expect("filter name must be valid UTF-8"))
.collect();
let mut filter_arr =
apr::tables::TypedArray::<*const i8>::new(&scratch_pool, filter_cstrs.len() as i32);
for s in &filter_cstrs {
filter_arr.push(s.as_ptr());
}
let filter_ptr: *const apr_sys::apr_array_header_t = if changelist_filter.is_empty() {
std::ptr::null()
} else {
unsafe { filter_arr.as_ptr() }
};
unsafe extern "C" fn cl_callback(
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 {
let cb = &mut *(baton
as *mut &mut dyn FnMut(&str, Option<&str>) -> Result<(), Error<'static>>);
let local_path = subversion_sys::svn_dirent_local_style(path, pool);
let path = std::ffi::CStr::from_ptr(local_path).to_str().unwrap_or("");
let cl = if changelist.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(changelist).to_str().unwrap_or(""))
};
match cb(path, cl) {
Ok(()) => std::ptr::null_mut(),
Err(e) => e.into_raw(),
}
}
let mut cb_ref: &mut dyn FnMut(&str, Option<&str>) -> Result<(), Error<'static>> =
&mut callback;
let baton = &mut cb_ref
as *mut &mut dyn FnMut(&str, Option<&str>) -> Result<(), Error<'static>>
as *mut std::ffi::c_void;
svn_result(unsafe {
subversion_sys::svn_wc_get_changelists(
self.ptr,
path_cstr.as_ptr(),
depth.into(),
filter_ptr,
Some(cl_callback),
baton,
None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
)
})
}
pub fn status(
&mut self,
local_abspath: &std::path::Path,
) -> Result<Status<'static>, Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let result_pool = apr::Pool::new();
let mut ptr: *mut subversion_sys::svn_wc_status3_t = std::ptr::null_mut();
with_tmp_pool(|scratch| {
svn_result(unsafe {
subversion_sys::svn_wc_status3(
&mut ptr,
self.ptr,
path_cstr.as_ptr(),
result_pool.as_mut_ptr(),
scratch.as_mut_ptr(),
)
})
})?;
Ok(Status {
ptr,
_pool: apr::pool::PoolHandle::owned(result_pool),
})
}
pub fn check_root(
&mut self,
local_abspath: &std::path::Path,
) -> Result<(bool, bool, crate::NodeKind), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let mut is_wcroot: subversion_sys::svn_boolean_t = 0;
let mut is_switched: subversion_sys::svn_boolean_t = 0;
let mut kind: subversion_sys::svn_node_kind_t =
subversion_sys::svn_node_kind_t_svn_node_unknown;
with_tmp_pool(|scratch| {
svn_result(unsafe {
subversion_sys::svn_wc_check_root(
&mut is_wcroot,
&mut is_switched,
&mut kind,
self.ptr,
path_cstr.as_ptr(),
scratch.as_mut_ptr(),
)
})
})?;
Ok((is_wcroot != 0, is_switched != 0, kind.into()))
}
pub fn restore(
&mut self,
local_abspath: &std::path::Path,
use_commit_times: bool,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
with_tmp_pool(|scratch| {
svn_result(unsafe {
subversion_sys::svn_wc_restore(
self.ptr,
path_cstr.as_ptr(),
use_commit_times as i32,
scratch.as_mut_ptr(),
)
})
})
}
pub fn get_ignores(
&mut self,
local_abspath: &std::path::Path,
) -> Result<Vec<String>, Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
let result_pool = apr::Pool::new();
let mut patterns: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
with_tmp_pool(|scratch| {
svn_result(unsafe {
subversion_sys::svn_wc_get_ignores2(
&mut patterns,
self.ptr,
path_cstr.as_ptr(),
std::ptr::null_mut(), result_pool.as_mut_ptr(),
scratch.as_mut_ptr(),
)
})
})?;
if patterns.is_null() {
return Ok(Vec::new());
}
let result =
unsafe { apr::tables::TypedArray::<*const std::os::raw::c_char>::from_ptr(patterns) }
.iter()
.map(|ptr| unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() })
.collect();
Ok(result)
}
pub fn remove_from_revision_control(
&mut self,
local_abspath: &std::path::Path,
destroy_wf: bool,
instant_error: bool,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
with_tmp_pool(|scratch| {
svn_result(unsafe {
subversion_sys::svn_wc_remove_from_revision_control2(
self.ptr,
path_cstr.as_ptr(),
destroy_wf as i32,
instant_error as i32,
None, std::ptr::null_mut(), scratch.as_mut_ptr(),
)
})
})
}
}
pub struct Notify {
ptr: *const subversion_sys::svn_wc_notify_t,
}
impl Notify {
unsafe fn from_ptr(ptr: *const subversion_sys::svn_wc_notify_t) -> Self {
Self { ptr }
}
pub fn action(&self) -> u32 {
unsafe { (*self.ptr).action as u32 }
}
pub fn path(&self) -> Option<&str> {
unsafe {
if (*self.ptr).path.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr((*self.ptr).path).to_str().unwrap())
}
}
}
pub fn kind(&self) -> crate::NodeKind {
unsafe { (*self.ptr).kind.into() }
}
pub fn mime_type(&self) -> Option<&str> {
unsafe {
if (*self.ptr).mime_type.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).mime_type)
.to_str()
.unwrap(),
)
}
}
}
pub fn lock(&self) -> Option<Lock> {
unsafe {
if (*self.ptr).lock.is_null() {
None
} else {
Some(Lock::from_ptr((*self.ptr).lock))
}
}
}
pub fn err(&self) -> Option<Error<'_>> {
unsafe {
if (*self.ptr).err.is_null() {
None
} else {
Some(Error::from_raw((*self.ptr).err).unwrap_err())
}
}
}
pub fn content_state(&self) -> u32 {
unsafe { (*self.ptr).content_state as u32 }
}
pub fn prop_state(&self) -> u32 {
unsafe { (*self.ptr).prop_state as u32 }
}
pub fn lock_state(&self) -> u32 {
unsafe { (*self.ptr).lock_state as u32 }
}
pub fn revision(&self) -> Option<crate::Revnum> {
unsafe { crate::Revnum::from_raw((*self.ptr).revision) }
}
pub fn changelist_name(&self) -> Option<&str> {
unsafe {
if (*self.ptr).changelist_name.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).changelist_name)
.to_str()
.unwrap(),
)
}
}
}
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 path_prefix(&self) -> Option<&str> {
unsafe {
if (*self.ptr).path_prefix.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).path_prefix)
.to_str()
.unwrap(),
)
}
}
}
pub fn prop_name(&self) -> Option<&str> {
unsafe {
if (*self.ptr).prop_name.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr((*self.ptr).prop_name)
.to_str()
.unwrap(),
)
}
}
}
pub fn old_revision(&self) -> Option<crate::Revnum> {
unsafe { crate::Revnum::from_raw((*self.ptr).old_revision) }
}
pub fn hunk_original_start(&self) -> u64 {
unsafe { (*self.ptr).hunk_original_start.into() }
}
pub fn hunk_original_length(&self) -> u64 {
unsafe { (*self.ptr).hunk_original_length.into() }
}
pub fn hunk_modified_start(&self) -> u64 {
unsafe { (*self.ptr).hunk_modified_start.into() }
}
pub fn hunk_modified_length(&self) -> u64 {
unsafe { (*self.ptr).hunk_modified_length.into() }
}
pub fn hunk_matched_line(&self) -> u64 {
unsafe { (*self.ptr).hunk_matched_line.into() }
}
pub fn hunk_fuzz(&self) -> u64 {
unsafe { (*self.ptr).hunk_fuzz.into() }
}
}
extern "C" fn wrap_conflict_func(
result: *mut *mut subversion_sys::svn_wc_conflict_result_t,
description: *const subversion_sys::svn_wc_conflict_description2_t,
baton: *mut std::ffi::c_void,
result_pool: *mut apr_sys::apr_pool_t,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || description.is_null() || result.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&*(baton
as *const Box<
dyn Fn(
&crate::conflict::ConflictDescription,
) -> Result<crate::conflict::ConflictResult, Error<'static>>,
>)
};
let desc = match unsafe { crate::conflict::ConflictDescription::from_raw(description) } {
Ok(d) => d,
Err(mut e) => return unsafe { e.detach() },
};
match callback(&desc) {
Ok(conflict_result) => {
unsafe {
*result = conflict_result.to_raw(result_pool);
}
std::ptr::null_mut()
}
Err(mut e) => unsafe { e.detach() },
}
}
extern "C" fn wrap_external_func(
baton: *mut std::ffi::c_void,
local_abspath: *const i8,
old_val: *const subversion_sys::svn_string_t,
new_val: *const subversion_sys::svn_string_t,
depth: subversion_sys::svn_depth_t,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || local_abspath.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&*(baton
as *const Box<
dyn Fn(
&str,
Option<&str>,
Option<&str>,
crate::Depth,
) -> Result<(), Error<'static>>,
>)
};
let path_str = unsafe {
std::ffi::CStr::from_ptr(local_abspath)
.to_str()
.unwrap_or("")
};
let old_str = if old_val.is_null() {
None
} else {
unsafe {
let data = (*old_val).data as *const u8;
let len = (*old_val).len;
std::str::from_utf8(std::slice::from_raw_parts(data, len)).ok()
}
};
let new_str = if new_val.is_null() {
None
} else {
unsafe {
let data = (*new_val).data as *const u8;
let len = (*new_val).len;
std::str::from_utf8(std::slice::from_raw_parts(data, len)).ok()
}
};
let depth_enum = crate::Depth::from(depth);
match callback(path_str, old_str, new_str, depth_enum) {
Ok(()) => std::ptr::null_mut(),
Err(mut e) => unsafe { e.detach() },
}
}
pub(crate) extern "C" fn wrap_notify_func(
baton: *mut std::ffi::c_void,
notify: *const subversion_sys::svn_wc_notify_t,
_pool: *mut apr_sys::apr_pool_t,
) {
if baton.is_null() || notify.is_null() {
return;
}
let callback = unsafe { &*(baton as *const Box<dyn Fn(&Notify)>) };
let notify_struct = unsafe { Notify::from_ptr(notify) };
callback(¬ify_struct);
}
extern "C" fn wrap_fetch_dirents_func(
baton: *mut std::ffi::c_void,
dirents: *mut *mut apr_sys::apr_hash_t,
repos_root_url: *const i8,
repos_relpath: *const i8,
result_pool: *mut apr_sys::apr_pool_t,
_scratch_pool: *mut apr_sys::apr_pool_t,
) -> *mut subversion_sys::svn_error_t {
if baton.is_null() || dirents.is_null() || repos_root_url.is_null() || repos_relpath.is_null() {
return std::ptr::null_mut();
}
let callback = unsafe {
&*(baton
as *const Box<
dyn Fn(
&str,
&str,
) -> Result<
std::collections::HashMap<String, crate::DirEntry>,
Error<'static>,
>,
>)
};
let root_url = unsafe {
std::ffi::CStr::from_ptr(repos_root_url)
.to_str()
.unwrap_or("")
};
let relpath = unsafe {
std::ffi::CStr::from_ptr(repos_relpath)
.to_str()
.unwrap_or("")
};
let pool = unsafe { apr::Pool::from_raw(result_pool) };
match callback(root_url, relpath) {
Ok(dirents_map) => {
let mut hash = apr::hash::Hash::new(&pool);
for (name, dirent) in dirents_map {
let svn_dirent = pool.alloc::<subversion_sys::svn_dirent_t>();
unsafe {
let svn_dirent_ptr = (*svn_dirent).as_mut_ptr();
std::ptr::write_bytes(svn_dirent_ptr, 0, 1);
(*svn_dirent_ptr).kind = dirent.kind().into();
(*svn_dirent_ptr).size = dirent.size();
(*svn_dirent_ptr).has_props = if dirent.has_props() { 1 } else { 0 };
(*svn_dirent_ptr).created_rev = dirent.created_rev().map(|r| r.0).unwrap_or(-1);
(*svn_dirent_ptr).time = dirent.time().into();
if let Some(author) = dirent.last_author() {
(*svn_dirent_ptr).last_author = pool.pstrdup(author);
}
hash.insert(name.as_bytes(), svn_dirent_ptr as *mut std::ffi::c_void);
}
}
unsafe {
*dirents = hash.as_mut_ptr();
}
std::ptr::null_mut()
}
Err(mut e) => unsafe { e.detach() },
}
}
pub struct CommittedQueue {
ptr: *mut subversion_sys::svn_wc_committed_queue_t,
_pool: apr::Pool<'static>,
}
impl Default for CommittedQueue {
fn default() -> Self {
Self::new()
}
}
impl CommittedQueue {
pub fn new() -> Self {
let pool = apr::Pool::new();
let ptr = unsafe { subversion_sys::svn_wc_committed_queue_create(pool.as_mut_ptr()) };
Self { ptr, _pool: pool }
}
pub(crate) fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_wc_committed_queue_t {
self.ptr
}
}
pub struct Lock {
ptr: *const subversion_sys::svn_lock_t,
_pool: Option<apr::Pool<'static>>,
}
impl Lock {
pub fn from_ptr(ptr: *const subversion_sys::svn_lock_t) -> Self {
Self { ptr, _pool: None }
}
pub fn new(path: Option<&str>, token: Option<&[u8]>) -> Self {
let pool = apr::Pool::new();
let lock_ptr = unsafe { subversion_sys::svn_lock_create(pool.as_mut_ptr()) };
if let Some(p) = path {
let cstr = std::ffi::CString::new(p).unwrap();
unsafe {
(*lock_ptr).path = apr_sys::apr_pstrdup(pool.as_mut_ptr(), cstr.as_ptr());
}
}
if let Some(t) = token {
let cstr = std::ffi::CString::new(t).unwrap();
unsafe {
(*lock_ptr).token = apr_sys::apr_pstrdup(pool.as_mut_ptr(), cstr.as_ptr());
}
}
Self {
ptr: lock_ptr as *const _,
_pool: Some(pool),
}
}
pub fn as_ptr(&self) -> *const subversion_sys::svn_lock_t {
self.ptr
}
pub fn path(&self) -> Option<&str> {
unsafe {
let p = (*self.ptr).path;
if p.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
}
}
}
pub fn token(&self) -> Option<&str> {
unsafe {
let t = (*self.ptr).token;
if t.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(t).to_str().unwrap())
}
}
}
pub fn owner(&self) -> Option<&str> {
unsafe {
let o = (*self.ptr).owner;
if o.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(o).to_str().unwrap())
}
}
}
pub fn comment(&self) -> Option<&str> {
unsafe {
let c = (*self.ptr).comment;
if c.is_null() {
None
} else {
Some(std::ffi::CStr::from_ptr(c).to_str().unwrap())
}
}
}
}
pub fn cleanup(
wc_path: &std::path::Path,
break_locks: bool,
fix_recorded_timestamps: bool,
clear_dav_cache: bool,
vacuum_pristines: bool,
_include_externals: bool,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(wc_path)?;
with_tmp_pool(|pool| -> Result<(), Error<'static>> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let err = unsafe {
subversion_sys::svn_wc_cleanup4(
ctx,
path_cstr.as_ptr(),
break_locks as i32,
fix_recorded_timestamps as i32,
clear_dav_cache as i32,
vacuum_pristines as i32,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
)
};
Error::from_raw(err)?;
Ok(())
})
}
pub fn add(
ctx: &mut Context,
path: &std::path::Path,
_depth: crate::Depth,
force: bool,
_no_ignore: bool,
_no_autoprops: bool,
_add_parents: bool,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
with_tmp_pool(|pool| unsafe {
let err = subversion_sys::svn_wc_add_from_disk3(
ctx.as_mut_ptr(),
path_cstr.as_ptr(),
std::ptr::null_mut(), force as i32,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
})
}
pub fn delete(
ctx: &mut Context,
path: &std::path::Path,
keep_local: bool,
delete_unversioned_target: bool,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
with_tmp_pool(|pool| unsafe {
let err = subversion_sys::svn_wc_delete4(
ctx.as_mut_ptr(),
path_cstr.as_ptr(),
keep_local as i32,
delete_unversioned_target as i32,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
})
}
pub fn revert(
ctx: &mut Context,
path: &std::path::Path,
depth: crate::Depth,
use_commit_times: bool,
clear_changelists: bool,
metadata_only: bool,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
with_tmp_pool(|pool| unsafe {
let err = subversion_sys::svn_wc_revert6(
ctx.as_mut_ptr(),
path_cstr.as_ptr(),
depth.into(),
use_commit_times as i32,
std::ptr::null(), clear_changelists as i32,
metadata_only as i32,
1, None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
})
}
pub fn copy_or_move(
ctx: &mut Context,
src: &std::path::Path,
dst: &std::path::Path,
is_move: bool,
metadata_only: bool,
) -> Result<(), Error<'static>> {
let src_cstr = crate::dirent::to_absolute_cstring(src)?;
let dst_cstr = crate::dirent::to_absolute_cstring(dst)?;
with_tmp_pool(|pool| unsafe {
if is_move {
let err = subversion_sys::svn_wc_move(
ctx.as_mut_ptr(),
src_cstr.as_ptr(),
dst_cstr.as_ptr(),
metadata_only as i32,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
} else {
let err = subversion_sys::svn_wc_copy3(
ctx.as_mut_ptr(),
src_cstr.as_ptr(),
dst_cstr.as_ptr(),
metadata_only as i32,
None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
}
})
}
pub fn resolve_conflict(
ctx: &mut Context,
path: &std::path::Path,
depth: crate::Depth,
resolve_text: bool,
_resolve_props: bool,
resolve_tree: bool,
conflict_choice: ConflictChoice,
) -> Result<(), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(path)?;
with_tmp_pool(|pool| unsafe {
let err = subversion_sys::svn_wc_resolved_conflict5(
ctx.as_mut_ptr(),
path_cstr.as_ptr(),
depth.into(),
resolve_text as i32,
std::ptr::null(), resolve_tree as i32,
conflict_choice.into(),
None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
);
Error::from_raw(err)
})
}
pub fn revision_status(
wc_path: &std::path::Path,
trail_url: Option<&str>,
committed: bool,
) -> Result<(i64, i64, bool, bool), Error<'static>> {
let path_cstr = crate::dirent::to_absolute_cstring(wc_path)?;
let trail_cstr = trail_url.map(std::ffi::CString::new).transpose()?;
with_tmp_pool(|pool| -> Result<(i64, i64, bool, bool), Error<'static>> {
let mut ctx = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_context_create(
&mut ctx,
std::ptr::null_mut(),
pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)
})?;
let mut status_ptr: *mut subversion_sys::svn_wc_revision_status_t = std::ptr::null_mut();
with_tmp_pool(|scratch_pool| {
let err = unsafe {
subversion_sys::svn_wc_revision_status2(
&mut status_ptr,
ctx,
path_cstr.as_ptr(),
trail_cstr.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
committed as i32,
None, std::ptr::null_mut(), pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
Error::from_raw(err)
})?;
if status_ptr.is_null() {
return Err(Error::from(std::io::Error::other(
"Failed to get revision status",
)));
}
let status = unsafe { *status_ptr };
Ok((
status.min_rev.into(),
status.max_rev.into(),
status.switched != 0,
status.modified != 0,
))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
pub enum ConflictChoice {
Postpone = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_postpone,
Base = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_base,
Theirs = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_full,
Mine = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_full,
TheirsConflict =
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_conflict,
MineConflict = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_conflict,
Merged = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_merged,
}
impl From<ConflictChoice> for subversion_sys::svn_wc_conflict_choice_t {
fn from(choice: ConflictChoice) -> Self {
choice as subversion_sys::svn_wc_conflict_choice_t
}
}
#[derive(Debug, Clone)]
pub struct ExternalItem {
pub target_dir: String,
pub url: String,
pub revision: crate::Revision,
pub peg_revision: crate::Revision,
}
pub fn parse_externals_description(
parent_directory: &str,
desc: &str,
canonicalize_url: bool,
) -> Result<Vec<ExternalItem>, Error<'static>> {
let pool = apr::Pool::new();
let parent_cstr = std::ffi::CString::new(parent_directory)
.map_err(|_| Error::from_message("Invalid parent directory"))?;
let desc_cstr = std::ffi::CString::new(desc)
.map_err(|_| Error::from_message("Invalid externals description"))?;
unsafe {
let mut externals_p: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
let err = subversion_sys::svn_wc_parse_externals_description3(
&mut externals_p,
parent_cstr.as_ptr(),
desc_cstr.as_ptr(),
canonicalize_url.into(),
pool.as_mut_ptr(),
);
svn_result(err)?;
if externals_p.is_null() {
return Ok(Vec::new());
}
let array =
apr::tables::TypedArray::<*const subversion_sys::svn_wc_external_item2_t>::from_ptr(
externals_p,
);
let mut result = Vec::new();
for item_ptr in array.iter() {
let item = &*item_ptr;
let target_dir = if item.target_dir.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(item.target_dir)
.to_str()
.map_err(|_| Error::from_message("Invalid target_dir UTF-8"))?
.to_string()
};
let url = if item.url.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(item.url)
.to_str()
.map_err(|_| Error::from_message("Invalid url UTF-8"))?
.to_string()
};
unsafe fn convert_revision(
rev: &subversion_sys::svn_opt_revision_t,
) -> crate::Revision {
match rev.kind {
subversion_sys::svn_opt_revision_kind_svn_opt_revision_unspecified => {
crate::Revision::Unspecified
}
subversion_sys::svn_opt_revision_kind_svn_opt_revision_number => {
crate::Revision::Number(crate::Revnum(*rev.value.number.as_ref()))
}
subversion_sys::svn_opt_revision_kind_svn_opt_revision_date => {
crate::Revision::Date(*rev.value.date.as_ref())
}
subversion_sys::svn_opt_revision_kind_svn_opt_revision_head => {
crate::Revision::Head
}
_ => crate::Revision::Unspecified,
}
}
result.push(ExternalItem {
target_dir,
url,
revision: convert_revision(&item.revision),
peg_revision: convert_revision(&item.peg_revision),
});
}
Ok(result)
}
}
pub fn get_default_ignores() -> Result<Vec<String>, crate::Error<'static>> {
let result_pool = apr::Pool::new();
let mut patterns: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
svn_result(unsafe {
subversion_sys::svn_wc_get_default_ignores(
&mut patterns,
std::ptr::null_mut(), result_pool.as_mut_ptr(),
)
})?;
if patterns.is_null() {
return Ok(Vec::new());
}
let result =
unsafe { apr::tables::TypedArray::<*const std::os::raw::c_char>::from_ptr(patterns) }
.iter()
.map(|ptr| unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() })
.collect();
Ok(result)
}
pub fn canonicalize_svn_prop(
propname: &str,
propval: &[u8],
path: &str,
kind: crate::NodeKind,
) -> Result<Vec<u8>, crate::Error<'static>> {
let pool = apr::Pool::new();
let propname_c = std::ffi::CString::new(propname)
.map_err(|_| crate::Error::from_message("property name contains interior NUL"))?;
let path_c = crate::dirent::canonicalize_path_or_url(path)
.map_err(|_| crate::Error::from_message("path contains interior NUL"))?;
let input = crate::string::BStr::from_bytes(propval, &pool);
let mut output: *const subversion_sys::svn_string_t = std::ptr::null();
svn_result(unsafe {
subversion_sys::svn_wc_canonicalize_svn_prop(
&mut output,
propname_c.as_ptr(),
input.as_ptr(),
path_c.as_ptr(),
kind.into(),
1, None, std::ptr::null_mut(), pool.as_mut_ptr(),
)
})?;
if output.is_null() {
return Ok(propval.to_vec());
}
let s = unsafe { &*output };
Ok(unsafe { std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec() })
}
pub fn transmit_text_deltas<'a>(
wc_ctx: &mut Context,
local_abspath: &str,
fulltext: bool,
file_editor: &crate::delta::WrapFileEditor<'a>,
) -> Result<(String, String), crate::Error<'static>> {
let result_pool = apr::Pool::new();
let scratch_pool = apr::Pool::new();
let local_abspath_c = crate::dirent::to_absolute_cstring(local_abspath)?;
let mut md5_checksum: *const subversion_sys::svn_checksum_t = std::ptr::null();
let mut sha1_checksum: *const subversion_sys::svn_checksum_t = std::ptr::null();
let (editor_ptr, baton_ptr) = file_editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_wc_transmit_text_deltas3(
&mut md5_checksum,
&mut sha1_checksum,
wc_ctx.ptr,
local_abspath_c.as_ptr(),
if fulltext { 1 } else { 0 },
editor_ptr,
baton_ptr,
result_pool.as_mut_ptr(),
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)?;
let md5_hex = if md5_checksum.is_null() {
String::new()
} else {
let checksum = crate::Checksum::from_raw(md5_checksum);
checksum.to_hex(&result_pool)
};
let sha1_hex = if sha1_checksum.is_null() {
String::new()
} else {
let checksum = crate::Checksum::from_raw(sha1_checksum);
checksum.to_hex(&result_pool)
};
Ok((md5_hex, sha1_hex))
}
pub fn transmit_prop_deltas_file<'a>(
wc_ctx: &mut Context,
local_abspath: &str,
file_editor: &crate::delta::WrapFileEditor<'a>,
) -> Result<(), crate::Error<'static>> {
let scratch_pool = apr::Pool::new();
let local_abspath_c = crate::dirent::to_absolute_cstring(local_abspath)?;
let (editor_ptr, baton_ptr) = file_editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_wc_transmit_prop_deltas2(
wc_ctx.ptr,
local_abspath_c.as_ptr(),
editor_ptr,
baton_ptr,
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(())
}
pub fn transmit_prop_deltas_dir<'a>(
wc_ctx: &mut Context,
local_abspath: &str,
dir_editor: &crate::delta::WrapDirectoryEditor<'a>,
) -> Result<(), crate::Error<'static>> {
let scratch_pool = apr::Pool::new();
let local_abspath_c = crate::dirent::to_absolute_cstring(local_abspath)?;
let (editor_ptr, baton_ptr) = dir_editor.as_raw_parts();
let err = unsafe {
subversion_sys::svn_wc_transmit_prop_deltas2(
wc_ctx.ptr,
local_abspath_c.as_ptr(),
editor_ptr,
baton_ptr,
scratch_pool.as_mut_ptr(),
)
};
svn_result(err)?;
Ok(())
}
#[cfg(all(test, feature = "client", feature = "repos"))]
mod tests {
use super::*;
use std::path::{Path, PathBuf};
use tempfile::tempdir;
fn ensure_timestamp_rollover() {
std::thread::sleep(std::time::Duration::from_micros(2000));
}
struct SvnTestFixture {
pub _repos_path: PathBuf,
pub wc_path: PathBuf,
pub url: String,
pub client_ctx: crate::client::Context,
pub temp_dir: tempfile::TempDir,
}
impl SvnTestFixture {
fn new() -> Self {
let temp_dir = tempfile::TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url = crate::path_to_file_url(&repos_path);
let mut client_ctx = crate::client::Context::new().unwrap();
let uri = crate::uri::Uri::new(&url).unwrap();
client_ctx
.checkout(uri, &wc_path, &Self::default_checkout_options())
.unwrap();
Self {
_repos_path: repos_path,
wc_path,
url,
client_ctx,
temp_dir,
}
}
fn default_checkout_options() -> crate::client::CheckoutOptions {
crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::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.client_ctx
.add(&file_path, &crate::client::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.client_ctx
.add(&dir_path, &crate::client::AddOptions::new())
.unwrap();
dir_path
}
fn wc_path_str(&self) -> &str {
self.wc_path
.to_str()
.expect("working copy path should be valid UTF-8")
}
fn get_wc_url(&mut self) -> String {
let wc_path = self
.wc_path
.to_str()
.expect("path should be valid UTF-8")
.to_string();
let mut url = None;
self.client_ctx
.info(
&wc_path,
&crate::client::InfoOptions::default(),
&|_, info| {
url = Some(info.url().to_string());
Ok(())
},
)
.unwrap();
url.expect("should have retrieved URL from info")
}
fn commit(&mut self) {
let wc_path_str = self.wc_path_str().to_string();
let commit_opts = crate::client::CommitOptions::default();
let revprops = std::collections::HashMap::new();
self.client_ctx
.commit(
&[wc_path_str.as_str()],
&commit_opts,
revprops,
None,
&mut |_info| Ok(()),
)
.unwrap();
}
}
fn create_repo(base: &Path, name: &str) -> (PathBuf, String) {
let repos_path = base.join(name);
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url = crate::path_to_file_url(&repos_path);
(repos_path, url)
}
#[test]
fn test_context_creation() {
let context = Context::new().unwrap();
assert!(!context.ptr.is_null());
}
#[test]
fn test_context_close() {
let mut context = Context::new().unwrap();
assert!(!context.ptr.is_null());
context.close();
assert!(context.ptr.is_null());
}
#[test]
fn test_context_close_idempotent() {
let mut context = Context::new().unwrap();
context.close();
context.close(); }
#[test]
fn test_adm_dir_default() {
let dir = get_adm_dir();
assert_eq!(dir, ".svn");
}
#[test]
fn test_is_adm_dir() {
assert!(is_adm_dir(".svn"));
assert!(!is_adm_dir("src"));
assert!(!is_adm_dir("test"));
assert!(!is_adm_dir(".git"));
}
#[test]
fn test_context_with_config() {
let config = std::ptr::null_mut();
Context::new_with_config(config).unwrap();
}
#[test]
fn test_check_wc() {
let dir = tempdir().unwrap();
let wc_path = dir.path();
let wc_format = check_wc(wc_path).unwrap();
assert_eq!(wc_format, None);
}
#[test]
fn test_ensure_adm() {
let dir = tempdir().unwrap();
let wc_path = dir.path();
let result = ensure_adm(
wc_path,
"", "file:///test/repo", "file:///test/repo", 0, );
result.unwrap();
}
#[test]
fn test_text_modified() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
let result = text_modified(&file_path, false);
assert!(result.is_err()); }
#[test]
fn test_props_modified() {
let dir = tempdir().unwrap();
let file_path = dir.path().join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
let result = props_modified(&file_path);
assert!(result.is_err()); }
#[test]
fn test_status_enum() {
assert_eq!(
StatusKind::Normal as u32,
subversion_sys::svn_wc_status_kind_svn_wc_status_normal as u32
);
assert_eq!(
StatusKind::Added as u32,
subversion_sys::svn_wc_status_kind_svn_wc_status_added as u32
);
assert_eq!(
StatusKind::Deleted as u32,
subversion_sys::svn_wc_status_kind_svn_wc_status_deleted as u32
);
let status = StatusKind::from(subversion_sys::svn_wc_status_kind_svn_wc_status_modified);
assert_eq!(status, StatusKind::Modified);
}
#[test]
fn test_schedule_enum() {
assert_eq!(
Schedule::Normal as u32,
subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal as u32
);
assert_eq!(
Schedule::Add as u32,
subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add as u32
);
let schedule = Schedule::from(subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete);
assert_eq!(schedule, Schedule::Delete);
}
#[test]
fn test_is_normal_prop() {
assert!(is_normal_prop("svn:keywords"));
assert!(is_normal_prop("svn:eol-style"));
assert!(is_normal_prop("svn:mime-type"));
assert!(!is_normal_prop("svn:entry:committed-rev"));
assert!(!is_normal_prop("svn:wc:ra_dav:version-url"));
}
#[test]
fn test_is_entry_prop() {
assert!(is_entry_prop("svn:entry:committed-rev"));
assert!(is_entry_prop("svn:entry:uuid"));
assert!(!is_entry_prop("svn:keywords"));
assert!(!is_entry_prop("user:custom"));
}
#[test]
fn test_is_wc_prop() {
assert!(is_wc_prop("svn:wc:ra_dav:version-url"));
assert!(!is_wc_prop("svn:keywords"));
assert!(!is_wc_prop("user:custom"));
}
#[test]
fn test_conflict_choice_enum() {
assert_eq!(
ConflictChoice::Postpone as i32,
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_postpone
);
assert_eq!(
ConflictChoice::Base as i32,
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_base
);
assert_eq!(
ConflictChoice::Theirs as i32,
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_full
);
assert_eq!(
ConflictChoice::Mine as i32,
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_full
);
}
#[test]
fn test_crop_tree_basic() {
let mut ctx = Context::new().unwrap();
let tempdir = tempdir().unwrap();
let result = ctx.crop_tree(tempdir.path(), crate::Depth::Files, None);
assert!(result.is_err());
}
#[test]
fn test_resolved_conflict_basic() {
let mut ctx = Context::new().unwrap();
let tempdir = tempdir().unwrap();
let result = ctx.resolved_conflict(
tempdir.path(),
crate::Depth::Infinity,
true, None, false, ConflictChoice::Mine,
None,
);
assert!(result.is_err());
}
#[test]
fn test_conflict_choice_conversion() {
let choice = ConflictChoice::Mine;
let svn_choice: subversion_sys::svn_wc_conflict_choice_t = choice.into();
assert_eq!(
svn_choice,
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_full
);
let choice = ConflictChoice::Theirs;
let svn_choice: subversion_sys::svn_wc_conflict_choice_t = choice.into();
assert_eq!(
svn_choice,
subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_full
);
}
#[test]
fn test_match_ignore_list() {
assert!(match_ignore_list("foo", &["foo", "bar"]).unwrap());
assert!(match_ignore_list("bar", &["foo", "bar"]).unwrap());
assert!(!match_ignore_list("baz", &["foo", "bar"]).unwrap());
assert!(match_ignore_list("foo", &["f*"]).unwrap());
assert!(match_ignore_list("foobar", &["f*"]).unwrap());
assert!(!match_ignore_list("bar", &["f*"]).unwrap());
assert!(match_ignore_list("test.txt", &["*.txt"]).unwrap());
assert!(match_ignore_list("file.txt", &["*.txt", "*.log"]).unwrap());
assert!(!match_ignore_list("test.rs", &["*.txt"]).unwrap());
assert!(!match_ignore_list("foo", &[]).unwrap());
}
#[test]
fn test_add_from_disk() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
let result = ctx.add_from_disk(&file_path, None, false, None);
assert!(result.is_err());
}
#[test]
fn test_add_repos_file() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let file_path = temp_dir.path().join("newfile.txt");
let content = b"file content\n";
let mut base_stream = crate::io::Stream::from(&content[..]);
let base_props: std::collections::HashMap<String, Vec<u8>> =
std::collections::HashMap::new();
let result = ctx.add_repos_file(
&file_path,
&mut base_stream,
None,
&base_props,
None,
None,
crate::Revnum(-1),
None,
);
assert!(result.is_err());
}
#[test]
fn test_move_path() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let src = temp_dir.path().join("src.txt");
let dst = temp_dir.path().join("dst.txt");
std::fs::write(&src, "content").unwrap();
let result = ctx.move_path(&src, &dst, false, false, None, None);
assert!(result.is_err());
}
#[test]
fn test_delete() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
let result = ctx.delete(&file_path, false, false, None, None);
assert!(result.is_err());
}
#[test]
fn test_get_switch_editor() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let options = SwitchEditorOptions::new();
let result = ctx.get_switch_editor(
temp_dir.path().to_str().unwrap(),
"",
"http://example.com/repo/branches/test",
options,
);
assert!(result.is_err());
}
#[test]
fn test_get_switch_editor_api() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let options = SwitchEditorOptions::new();
let result = ctx.get_switch_editor(
temp_dir.path().to_str().unwrap(),
"",
"http://example.com/svn/trunk",
options,
);
assert!(result.is_err());
}
#[test]
fn test_get_switch_editor_with_target() {
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let options = SwitchEditorOptions {
use_commit_times: true,
depth: crate::Depth::Files,
depth_is_sticky: true,
allow_unver_obstructions: true,
..Default::default()
};
let result = ctx.get_switch_editor(
temp_dir.path().to_str().unwrap(),
"subdir", "http://example.com/svn/branches/test",
options,
);
assert!(result.is_err());
}
#[test]
fn test_get_diff_editor() {
let temp_dir = tempfile::tempdir().unwrap();
let wc_path = temp_dir.path().join("wc");
std::fs::create_dir(&wc_path).unwrap();
ensure_adm(
&wc_path,
"test-uuid",
"file:///tmp/test-repo",
"file:///tmp/test-repo",
0,
)
.unwrap();
let mut ctx = Context::new().unwrap();
let mut callbacks = RecordingDiffCallbacks::new();
let result = ctx.get_diff_editor(
wc_path.to_str().unwrap(),
wc_path.to_str().unwrap(),
&mut callbacks,
false, crate::Depth::Infinity,
false, false, false, );
result.unwrap();
}
#[test]
fn test_get_diff_editor_with_options() {
let temp_dir = tempfile::tempdir().unwrap();
let wc_path = temp_dir.path().join("wc");
std::fs::create_dir(&wc_path).unwrap();
ensure_adm(
&wc_path,
"test-uuid",
"file:///tmp/test-repo",
"file:///tmp/test-repo",
0,
)
.unwrap();
let mut ctx = Context::new().unwrap();
let mut callbacks = RecordingDiffCallbacks::new();
let result = ctx.get_diff_editor(
wc_path.to_str().unwrap(),
wc_path.to_str().unwrap(),
&mut callbacks,
true, crate::Depth::Empty,
true, true, true, );
result.unwrap();
}
#[test]
fn test_update_editor_trait() {
use crate::delta::Editor;
fn check_editor_impl<T: Editor>() {}
check_editor_impl::<UpdateEditor<'_>>();
}
#[test]
fn test_committed_queue() {
let queue = CommittedQueue::new();
assert!(!queue.ptr.is_null());
let temp_dir = tempfile::tempdir().unwrap();
let mut ctx = Context::new().unwrap();
let mut queue = CommittedQueue::new();
let file_path = temp_dir.path().join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
let result = ctx.queue_committed(
&file_path, false, true, &mut queue, None, false, false, None,
);
assert!(result.is_err());
}
#[test]
fn test_wc_prop_operations() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("test.txt", "test content");
fixture
.client_ctx
.propset(
"test:property",
Some(b"test value"),
file_path.to_str().expect("file path should be valid UTF-8"),
&crate::client::PropSetOptions::default(),
)
.unwrap();
let mut wc_ctx = Context::new().unwrap();
let value = wc_ctx.prop_get(&file_path, "test:property").unwrap();
assert_eq!(value, Some(b"test value".to_vec()));
let missing = wc_ctx.prop_get(&file_path, "test:missing").unwrap();
assert_eq!(missing, None);
let props = wc_ctx.prop_list(&file_path).unwrap();
assert!(props.contains_key("test:property"));
assert_eq!(props.get("test:property").unwrap(), b"test value");
}
#[test]
fn test_get_prop_diffs() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("test.txt", "test content");
fixture
.client_ctx
.propset(
"test:prop1",
Some(b"value1"),
file_path.to_str().expect("file path should be valid UTF-8"),
&crate::client::PropSetOptions::default(),
)
.unwrap();
let mut wc_ctx = Context::new().unwrap();
let (changes, original) = wc_ctx.get_prop_diffs(&file_path).unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].name, "test:prop1");
assert_eq!(changes[0].value, Some(b"value1".to_vec()));
if let Some(orig) = original {
assert!(!orig.contains_key("test:prop1"));
}
}
#[test]
fn test_read_kind() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("test.txt", "test content");
let dir_path = fixture.add_dir("subdir");
let mut wc_ctx = Context::new().unwrap();
let kind = wc_ctx.read_kind(&fixture.wc_path, false, false).unwrap();
assert_eq!(kind, crate::NodeKind::Dir);
let kind = wc_ctx.read_kind(&file_path, false, false).unwrap();
assert_eq!(kind, crate::NodeKind::File);
let kind = wc_ctx.read_kind(&dir_path, false, false).unwrap();
assert_eq!(kind, crate::NodeKind::Dir);
let nonexistent = fixture.wc_path.join("nonexistent");
let kind = wc_ctx.read_kind(&nonexistent, false, false).unwrap();
assert_eq!(kind, crate::NodeKind::None);
}
#[test]
fn test_is_wc_root() {
let mut fixture = SvnTestFixture::new();
let subdir = fixture.add_dir("subdir");
let mut wc_ctx = Context::new().unwrap();
let is_root = wc_ctx.is_wc_root(&fixture.wc_path).unwrap();
assert!(is_root, "Working copy root should be detected as WC root");
let is_root = wc_ctx.is_wc_root(&subdir).unwrap();
assert!(!is_root, "Subdirectory should not be a WC root");
}
#[test]
fn test_get_pristine_contents() {
use std::io::Read;
let mut fixture = SvnTestFixture::new();
let original_content = "original content";
let file_path = fixture.add_file("test.txt", original_content);
fixture.commit();
let modified_content = "modified content";
std::fs::write(&file_path, modified_content).unwrap();
let mut wc_ctx = Context::new().unwrap();
let pristine_stream = wc_ctx
.get_pristine_contents(file_path.to_str().expect("file path should be valid UTF-8"))
.unwrap();
assert!(
pristine_stream.is_some(),
"Should have pristine contents for committed file"
);
let mut pristine_stream = pristine_stream.unwrap();
let mut pristine_content = String::new();
pristine_stream
.read_to_string(&mut pristine_content)
.unwrap();
assert_eq!(
pristine_content, original_content,
"Pristine content should match original"
);
let new_file = fixture.add_file("new.txt", "new file content");
let pristine = wc_ctx
.get_pristine_contents(
new_file
.to_str()
.expect("new file path should be valid UTF-8"),
)
.unwrap();
assert!(
pristine.is_none(),
"Newly added file should have no pristine contents"
);
}
#[test]
fn test_exclude() {
use std::cell::{Cell, RefCell};
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let checkout_opts = crate::client::CheckoutOptions {
revision: crate::Revision::Head,
peg_revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
};
client_ctx.checkout(url, &wc_path, &checkout_opts).unwrap();
let subdir = wc_path.join("subdir");
std::fs::create_dir(&subdir).unwrap();
let file_in_subdir = subdir.join("file.txt");
std::fs::write(&file_in_subdir, "content").unwrap();
client_ctx
.add(&subdir, &crate::client::AddOptions::new())
.unwrap();
let commit_opts = crate::client::CommitOptions::default();
let revprops = std::collections::HashMap::new();
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&commit_opts,
revprops,
None,
&mut |_info| Ok(()),
)
.unwrap();
assert!(subdir.exists(), "Subdirectory should exist before exclude");
assert!(file_in_subdir.exists(), "File should exist before exclude");
let mut wc_ctx = Context::new().unwrap();
let notifications = RefCell::new(Vec::new());
let result = wc_ctx.exclude(
&subdir,
None,
Some(&|notify: &Notify| {
notifications
.borrow_mut()
.push(format!("{:?}", notify.action()));
}),
);
assert!(result.is_ok(), "Exclude should succeed: {:?}", result);
assert!(
!subdir.exists(),
"Subdirectory should not exist after exclude"
);
assert!(
!notifications.borrow().is_empty(),
"Should have received notifications"
);
let kind = wc_ctx.read_kind(&subdir, false, false).unwrap();
assert_eq!(
kind,
crate::NodeKind::None,
"Excluded directory should show as None"
);
let subdir2 = wc_path.join("subdir2");
std::fs::create_dir(&subdir2).unwrap();
client_ctx
.add(&subdir2, &crate::client::AddOptions::new())
.unwrap();
let commit_opts = crate::client::CommitOptions::default();
let revprops = std::collections::HashMap::new();
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&commit_opts,
revprops,
None,
&mut |_info| Ok(()),
)
.unwrap();
let cancel_called = Cell::new(false);
let result = wc_ctx.exclude(
&subdir2,
Some(&|| {
cancel_called.set(true);
Ok(()) }),
None,
);
assert!(
result.is_ok(),
"Exclude with cancel callback should succeed"
);
assert!(
cancel_called.get(),
"Cancel callback should have been called"
);
assert!(!subdir2.exists(), "Second subdirectory should be excluded");
}
#[test]
fn test_get_pristine_props() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let checkout_opts = crate::client::CheckoutOptions {
revision: crate::Revision::Head,
peg_revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
};
client_ctx.checkout(url, &wc_path, &checkout_opts).unwrap();
let file_path = wc_path.join("test.txt");
std::fs::write(&file_path, "original content").unwrap();
client_ctx
.add(&file_path, &crate::client::AddOptions::new())
.unwrap();
let propset_opts = crate::client::PropSetOptions::default();
client_ctx
.propset(
"svn:eol-style",
Some(b"native"),
file_path.to_str().unwrap(),
&propset_opts,
)
.unwrap();
client_ctx
.propset(
"custom:prop",
Some(b"custom value"),
file_path.to_str().unwrap(),
&propset_opts,
)
.unwrap();
let commit_opts = crate::client::CommitOptions::default();
let revprops = std::collections::HashMap::new();
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&commit_opts,
revprops,
None,
&mut |_info| Ok(()),
)
.unwrap();
let mut wc_ctx = Context::new().unwrap();
let pristine_props = wc_ctx
.get_pristine_props(file_path.to_str().unwrap())
.unwrap();
assert!(
pristine_props.is_some(),
"Committed file should have pristine props"
);
let props = pristine_props.unwrap();
assert!(
props.contains_key("svn:eol-style"),
"Should have svn:eol-style property"
);
assert_eq!(
props.get("svn:eol-style").unwrap(),
b"native",
"svn:eol-style should be 'native'"
);
assert!(
props.contains_key("custom:prop"),
"Should have custom:prop property"
);
assert_eq!(
props.get("custom:prop").unwrap(),
b"custom value",
"custom:prop should be 'custom value'"
);
client_ctx
.propset(
"svn:eol-style",
Some(b"LF"),
file_path.to_str().unwrap(),
&propset_opts,
)
.unwrap();
let pristine_props = wc_ctx
.get_pristine_props(file_path.to_str().unwrap())
.unwrap()
.unwrap();
assert_eq!(
pristine_props.get("svn:eol-style").unwrap(),
b"native",
"Pristine svn:eol-style should still be 'native'"
);
client_ctx
.propset(
"custom:prop",
None,
file_path.to_str().unwrap(),
&propset_opts,
)
.unwrap();
let pristine_props = wc_ctx
.get_pristine_props(file_path.to_str().unwrap())
.unwrap()
.unwrap();
assert!(
pristine_props.contains_key("custom:prop"),
"Pristine props should still contain deleted property"
);
let new_file = wc_path.join("new.txt");
std::fs::write(&new_file, "new content").unwrap();
client_ctx
.add(&new_file, &crate::client::AddOptions::new())
.unwrap();
let pristine = wc_ctx
.get_pristine_props(new_file.to_str().unwrap())
.unwrap();
if let Some(props) = pristine {
assert!(
props.is_empty() || props.len() == 0,
"Newly added file pristine props should be empty if not None"
);
}
let nonexistent = wc_path.join("nonexistent.txt");
let result = wc_ctx.get_pristine_props(nonexistent.to_str().unwrap());
assert!(result.is_err(), "Non-existent file should return an error");
}
#[test]
fn test_conflicted() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc1_path = temp_dir.path().join("wc1");
let wc2_path = temp_dir.path().join("wc2");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx1 = crate::client::Context::new().unwrap();
let checkout_opts = crate::client::CheckoutOptions {
revision: crate::Revision::Head,
peg_revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
};
client_ctx1
.checkout(url.clone(), &wc1_path, &checkout_opts)
.unwrap();
let file_path1 = wc1_path.join("test.txt");
std::fs::write(&file_path1, "line1\nline2\nline3\n").unwrap();
client_ctx1
.add(&file_path1, &crate::client::AddOptions::new())
.unwrap();
let commit_opts = crate::client::CommitOptions::default();
let revprops = std::collections::HashMap::new();
client_ctx1
.commit(
&[wc1_path.to_str().unwrap()],
&commit_opts,
revprops.clone(),
None,
&mut |_info| Ok(()),
)
.unwrap();
let mut client_ctx2 = crate::client::Context::new().unwrap();
client_ctx2
.checkout(url.clone(), &wc2_path, &checkout_opts)
.unwrap();
std::fs::write(&file_path1, "line1 modified in wc1\nline2\nline3\n").unwrap();
client_ctx1
.commit(
&[wc1_path.to_str().unwrap()],
&commit_opts,
revprops.clone(),
None,
&mut |_info| Ok(()),
)
.unwrap();
let file_path2 = wc2_path.join("test.txt");
std::fs::write(&file_path2, "line1 modified in wc2\nline2\nline3\n").unwrap();
ensure_timestamp_rollover();
let update_opts = crate::client::UpdateOptions::default();
let _result = client_ctx2.update(
&[wc2_path.to_str().unwrap()],
crate::Revision::Head,
&update_opts,
);
let mut wc_ctx = Context::new().unwrap();
let (text_conflicted, prop_conflicted, tree_conflicted) =
wc_ctx.conflicted(file_path2.to_str().unwrap()).unwrap();
assert!(
text_conflicted,
"File should have text conflict after conflicting update"
);
assert!(!prop_conflicted, "File should not have property conflict");
assert!(!tree_conflicted, "File should not have tree conflict");
let propfile_path1 = wc1_path.join("proptest.txt");
std::fs::write(&propfile_path1, "content").unwrap();
client_ctx1
.add(&propfile_path1, &crate::client::AddOptions::new())
.unwrap();
let propset_opts = crate::client::PropSetOptions::default();
client_ctx1
.propset(
"custom:prop",
Some(b"value1"),
propfile_path1.to_str().unwrap(),
&propset_opts,
)
.unwrap();
client_ctx1
.commit(
&[wc1_path.to_str().unwrap()],
&commit_opts,
revprops.clone(),
None,
&mut |_info| Ok(()),
)
.unwrap();
let _result = client_ctx2.update(
&[wc2_path.to_str().unwrap()],
crate::Revision::Head,
&update_opts,
);
let propfile_path2 = wc2_path.join("proptest.txt");
client_ctx2
.propset(
"custom:prop",
Some(b"value2"),
propfile_path2.to_str().unwrap(),
&propset_opts,
)
.unwrap();
client_ctx1
.propset(
"custom:prop",
Some(b"value1_modified"),
propfile_path1.to_str().unwrap(),
&propset_opts,
)
.unwrap();
client_ctx1
.commit(
&[wc1_path.to_str().unwrap()],
&commit_opts,
revprops.clone(),
None,
&mut |_info| Ok(()),
)
.unwrap();
let _result = client_ctx2.update(
&[wc2_path.to_str().unwrap()],
crate::Revision::Head,
&update_opts,
);
let (_text_conflicted, prop_conflicted, _tree_conflicted) =
wc_ctx.conflicted(propfile_path2.to_str().unwrap()).unwrap();
if prop_conflicted {
assert!(
prop_conflicted,
"File should have property conflict after conflicting property update"
);
}
let clean_file = wc1_path.join("clean.txt");
std::fs::write(&clean_file, "no conflicts here").unwrap();
client_ctx1
.add(&clean_file, &crate::client::AddOptions::new())
.unwrap();
let (text_conflicted, prop_conflicted, tree_conflicted) =
wc_ctx.conflicted(clean_file.to_str().unwrap()).unwrap();
assert!(!text_conflicted, "Clean file should not have text conflict");
assert!(
!prop_conflicted,
"Clean file should not have property conflict"
);
assert!(!tree_conflicted, "Clean file should not have tree conflict");
let nonexistent = wc1_path.join("nonexistent.txt");
let result = wc_ctx.conflicted(nonexistent.to_str().unwrap());
assert!(result.is_err(), "Non-existent file should return an error");
}
#[test]
fn test_copy_or_move() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let wc_path = td.path().join("wc");
crate::repos::Repos::create(&repo_path).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let url_str = crate::path_to_file_url(&repo_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let test_file = wc_path.join("test.txt");
std::fs::write(&test_file, "test content").unwrap();
client_ctx
.add(&test_file, &crate::client::AddOptions::new())
.unwrap();
let mut wc_ctx = Context::new().unwrap();
let copy_dest = wc_path.join("test_copy.txt");
let result = copy_or_move(&mut wc_ctx, &test_file, ©_dest, false, false);
assert!(result.is_err());
let move_dest = wc_path.join("test_moved.txt");
let result = copy_or_move(&mut wc_ctx, &test_file, &move_dest, true, false);
assert!(result.is_err());
}
#[test]
fn test_revert() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let wc_path = td.path().join("wc");
crate::repos::Repos::create(&repo_path).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let url_str = crate::path_to_file_url(&repo_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let test_file = wc_path.join("test.txt");
std::fs::write(&test_file, "original content").unwrap();
client_ctx
.add(&test_file, &crate::client::AddOptions::new())
.unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = revert(
&mut wc_ctx,
&test_file,
crate::Depth::Empty,
false,
false,
false,
);
assert!(result.is_err());
}
#[test]
fn test_cleanup() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let wc_path = td.path().join("wc");
crate::repos::Repos::create(&repo_path).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let url_str = crate::path_to_file_url(&repo_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let result = cleanup(&wc_path, false, false, false, false, false);
result.unwrap();
cleanup(&wc_path, true, false, false, false, false).unwrap();
cleanup(&wc_path, false, true, false, false, false).unwrap();
}
#[test]
fn test_get_actual_target() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let wc_path = td.path().join("wc");
crate::repos::Repos::create(&repo_path).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let url_str = crate::path_to_file_url(&repo_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let (anchor, target) = get_actual_target(&wc_path).unwrap();
assert!(!anchor.is_empty() || !target.is_empty());
}
#[test]
fn test_walk_status() {
let td = tempfile::tempdir().unwrap();
let repo_path = td.path().join("repo");
let wc_path = td.path().join("wc");
crate::repos::Repos::create(&repo_path).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
let url_str = crate::path_to_file_url(&repo_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let test_file = wc_path.join("test.txt");
std::fs::write(&test_file, "test content").unwrap();
client_ctx
.add(&test_file, &crate::client::AddOptions::new())
.unwrap();
let mut wc_ctx = Context::new().unwrap();
let mut status_count = 0;
let result = wc_ctx.walk_status(
&wc_path,
crate::Depth::Infinity,
true, false, false, None, |_path, _status| {
status_count += 1;
Ok(())
},
);
result.unwrap();
assert!(status_count >= 1, "Should have at least one status entry");
}
#[test]
fn test_wc_version() {
let version = version();
assert!(version.major() > 0);
}
#[test]
#[ignore]
fn test_set_and_get_adm_dir() {
set_adm_dir("_svn").unwrap();
let dir = get_adm_dir();
assert_eq!(dir, "_svn");
set_adm_dir(".svn").unwrap();
let dir = get_adm_dir();
assert_eq!(dir, ".svn");
}
#[test]
fn test_context_add() {
let fixture = SvnTestFixture::new();
let new_file = fixture.wc_path.join("newfile.txt");
std::fs::write(&new_file, b"test content").unwrap();
let mut wc_ctx = Context::new().unwrap();
let new_file_abs = new_file.canonicalize().unwrap();
let result = wc_ctx.add(
new_file_abs
.to_str()
.expect("file path should be valid UTF-8"),
crate::Depth::Infinity,
None,
None,
);
assert!(result.is_err());
let err_str = format!("{:?}", result.err().unwrap());
assert!(
err_str.to_lowercase().contains("lock"),
"Expected lock error, got: {}",
err_str
);
}
#[test]
fn test_context_relocate() {
let tmp_dir = tempfile::tempdir().unwrap();
let repos_path = tmp_dir.path().join("repo");
let wc_path = tmp_dir.path().join("wc");
crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let repos_path2 = tmp_dir.path().join("repo_moved");
std::fs::rename(&repos_path, &repos_path2).unwrap();
let repos_url2 = crate::path_to_file_url(&repos_path2);
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.relocate(wc_path.to_str().unwrap(), &url_str, &repos_url2);
assert!(result.is_ok(), "relocate() failed: {:?}", result.err());
}
#[test]
fn test_context_upgrade() {
let fixture = SvnTestFixture::new();
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.upgrade(fixture.wc_path_str());
assert!(result.is_ok(), "upgrade() failed: {:?}", result.err());
}
#[test]
fn test_get_update_editor4_with_callbacks() {
let tmp_dir = tempfile::tempdir().unwrap();
let repos_path = tmp_dir.path().join("repo");
let wc_path = tmp_dir.path().join("wc");
crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let cancel_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let cancel_called_clone = cancel_called.clone();
let mut wc_ctx = Context::new().unwrap();
let options = UpdateEditorOptions {
cancel_func: Some(Box::new(move || {
cancel_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
Ok(())
})),
..Default::default()
};
let result = get_update_editor4(&mut wc_ctx, wc_path.to_str().unwrap(), "", options);
assert!(
result.is_ok(),
"get_update_editor4 failed: {:?}",
result.err()
);
}
#[test]
fn test_get_switch_editor_with_callbacks() {
let tmp_dir = tempfile::tempdir().unwrap();
let repos_path = tmp_dir.path().join("repo");
let wc_path = tmp_dir.path().join("wc");
crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url.clone(),
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let notify_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let notify_called_clone = notify_called.clone();
let mut wc_ctx = Context::new().unwrap();
let options = SwitchEditorOptions {
notify_func: Some(Box::new(move |_notify| {
notify_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
})),
..Default::default()
};
let result =
wc_ctx.get_switch_editor(wc_path.to_str().unwrap(), "", url_str.as_str(), options);
assert!(
result.is_ok(),
"get_switch_editor failed: {:?}",
result.err()
);
}
#[test]
fn test_get_update_editor4_server_performs_filtering() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(temp_dir.path()).unwrap();
let mut wc_ctx = Context::new().unwrap();
let options = UpdateEditorOptions {
server_performs_filtering: true,
..Default::default()
};
let result =
get_update_editor4(&mut wc_ctx, temp_dir.path().to_str().unwrap(), "", options);
assert!(result.is_err());
}
#[test]
fn test_get_update_editor4_clean_checkout() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(temp_dir.path()).unwrap();
let mut wc_ctx = Context::new().unwrap();
let options = UpdateEditorOptions {
clean_checkout: true,
..Default::default()
};
let result =
get_update_editor4(&mut wc_ctx, temp_dir.path().to_str().unwrap(), "", options);
assert!(result.is_err());
}
#[test]
fn test_get_switch_editor_server_performs_filtering() {
let temp_dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(temp_dir.path()).unwrap();
let mut wc_ctx = Context::new().unwrap();
let options = SwitchEditorOptions {
server_performs_filtering: true,
..Default::default()
};
let result = wc_ctx.get_switch_editor(
temp_dir.path().to_str().unwrap(),
"",
"http://example.com/svn/trunk",
options,
);
assert!(result.is_err());
}
#[test]
fn test_parse_externals_description() {
let desc = "^/trunk/lib lib";
let items = parse_externals_description("/parent", desc, true).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].target_dir, "lib");
assert!(items[0].url.contains("trunk/lib"));
}
#[test]
fn test_parse_externals_description_with_revision() {
let desc = "-r42 http://example.com/svn/trunk external_dir";
let items = parse_externals_description("/parent", desc, false).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].target_dir, "external_dir");
assert_eq!(items[0].url, "http://example.com/svn/trunk");
}
#[test]
fn test_parse_externals_description_empty() {
let items = parse_externals_description("/parent", "", true).unwrap();
assert!(items.is_empty());
}
#[test]
fn test_wc_add_function() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let file_path = wc_path.join("new_file.txt");
std::fs::write(&file_path, "test content").unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = add(
&mut wc_ctx,
&file_path,
crate::Depth::Infinity,
false, false, false, false, );
assert!(result.is_err(), "add() should fail without write lock");
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"Expected lock-related error, got: {}",
err_msg
);
}
#[test]
fn test_wc_delete_keep_local() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let file_path = wc_path.join("to_delete.txt");
std::fs::write(&file_path, "test content").unwrap();
client_ctx
.add(&file_path, &crate::client::AddOptions::new())
.unwrap();
let mut committed = false;
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&crate::client::CommitOptions::default(),
std::collections::HashMap::from([("svn:log", "Add test file")]),
None,
&mut |_info| {
committed = true;
Ok(())
},
)
.unwrap();
assert!(committed);
assert!(file_path.exists());
let mut wc_ctx = Context::new().unwrap();
let result = delete(
&mut wc_ctx,
&file_path,
true, false, );
assert!(result.is_err(), "delete() should fail without write lock");
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"Expected lock-related error, got: {}",
err_msg
);
assert!(file_path.exists());
}
#[test]
fn test_wc_delete_remove_local() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let file_path = wc_path.join("to_remove.txt");
std::fs::write(&file_path, "test content").unwrap();
client_ctx
.add(&file_path, &crate::client::AddOptions::new())
.unwrap();
let mut committed = false;
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&crate::client::CommitOptions::default(),
std::collections::HashMap::from([("svn:log", "Add test file")]),
None,
&mut |_info| {
committed = true;
Ok(())
},
)
.unwrap();
assert!(committed);
assert!(file_path.exists());
let mut wc_ctx = Context::new().unwrap();
let result = delete(
&mut wc_ctx,
&file_path,
false, false, );
assert!(result.is_err(), "delete() should fail without write lock");
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"Expected lock-related error, got: {}",
err_msg
);
assert!(file_path.exists());
}
#[test]
fn test_revision_status_empty_wc() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let result = revision_status(&wc_path, None, false);
assert!(result.is_ok(), "revision_status should succeed on empty WC");
let (min_rev, max_rev, is_switched, is_modified) = result.unwrap();
assert_eq!(min_rev, 0, "min_rev should be 0 for empty WC");
assert_eq!(max_rev, 0, "max_rev should be 0 for empty WC");
assert_eq!(is_switched, false, "should not be switched");
assert_eq!(is_modified, false, "should not be modified");
}
#[test]
fn test_revision_status_with_modifications() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let file_path = wc_path.join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
client_ctx
.add(&file_path, &crate::client::AddOptions::new())
.unwrap();
let result = revision_status(&wc_path, None, false);
assert!(
result.is_ok(),
"revision_status should succeed with modifications"
);
let (min_rev, max_rev, is_switched, is_modified) = result.unwrap();
assert_eq!(min_rev, 0, "min_rev should be 0");
assert_eq!(max_rev, 0, "max_rev should be 0");
assert_eq!(is_switched, false, "should not be switched");
assert_eq!(is_modified, true, "should be modified after adding file");
}
#[test]
fn test_revision_status_after_commit() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let file_path = wc_path.join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
client_ctx
.add(&file_path, &crate::client::AddOptions::new())
.unwrap();
let mut committed = false;
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&crate::client::CommitOptions::default(),
std::collections::HashMap::from([("svn:log", "Add test file")]),
None,
&mut |_info| {
committed = true;
Ok(())
},
)
.unwrap();
assert!(committed, "commit should have been called");
let result = revision_status(&wc_path, None, false);
assert!(
result.is_ok(),
"revision_status should succeed after commit"
);
let (min_rev, max_rev, is_switched, is_modified) = result.unwrap();
assert!(
max_rev >= 1,
"max_rev should be at least 1 after commit, got {}",
max_rev
);
assert!(
min_rev <= max_rev,
"min_rev ({}) should be <= max_rev ({})",
min_rev,
max_rev
);
assert_eq!(is_switched, false, "should not be switched");
assert_eq!(is_modified, false, "should not be modified after commit");
}
#[test]
fn test_resolve_conflict_function() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_wc_path = temp_dir.path().join("not_a_wc");
std::fs::create_dir(&non_wc_path).unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = resolve_conflict(
&mut wc_ctx,
&non_wc_path,
crate::Depth::Empty,
true, true, false, ConflictChoice::Postpone,
);
assert!(
result.is_err(),
"resolve_conflict() should fail on non-WC path, proving it calls the C library"
);
}
#[test]
fn test_status_methods() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let repos_path = temp_dir.path().join("repos");
let wc_path = temp_dir.path().join("wc");
let _repos = crate::repos::Repos::create(&repos_path).unwrap();
let url_str = crate::path_to_file_url(&repos_path);
let url = crate::uri::Uri::new(&url_str).unwrap();
let mut client_ctx = crate::client::Context::new().unwrap();
client_ctx
.checkout(
url,
&wc_path,
&crate::client::CheckoutOptions {
peg_revision: crate::Revision::Head,
revision: crate::Revision::Head,
depth: crate::Depth::Infinity,
ignore_externals: false,
allow_unver_obstructions: false,
},
)
.unwrap();
let file_path = wc_path.join("test.txt");
std::fs::write(&file_path, "test content").unwrap();
client_ctx
.add(&file_path, &crate::client::AddOptions::new())
.unwrap();
let mut committed = false;
client_ctx
.commit(
&[wc_path.to_str().unwrap()],
&crate::client::CommitOptions::default(),
std::collections::HashMap::from([("svn:log", "Add test file")]),
None,
&mut |_info| {
committed = true;
Ok(())
},
)
.unwrap();
assert!(committed);
let mut wc_ctx = Context::new().unwrap();
let mut found_status = false;
wc_ctx
.walk_status(
&file_path,
crate::Depth::Empty,
true, false, false, None, |_path, status| {
found_status = true;
assert_eq!(status.copied(), false, "File should not be copied");
assert_eq!(status.switched(), false, "File should not be switched");
assert_eq!(status.locked(), false, "File should not be locked");
assert_eq!(
status.node_status(),
StatusKind::Normal,
"File should be normal"
);
assert!(status.revision().0 >= 0, "Revision should be >= 0");
assert_eq!(
status.repos_relpath(),
Some("test.txt".to_string()),
"repos_relpath should match"
);
Ok(())
},
)
.unwrap();
assert!(found_status, "Should have found file status");
}
#[test]
fn test_status_added_file() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("added.txt", "test content");
let mut wc_ctx = Context::new().unwrap();
let mut found_status = false;
wc_ctx
.walk_status(
&file_path,
crate::Depth::Empty,
true, false, false, None, |_path, status| {
found_status = true;
assert_eq!(
status.node_status(),
StatusKind::Added,
"File should be added"
);
assert_eq!(
status.copied(),
false,
"Added file should not be marked as copied"
);
Ok(())
},
)
.unwrap();
assert!(found_status, "Should have found file status");
}
#[test]
fn test_cleanup_actually_executes() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_existent = temp_dir.path().join("does_not_exist");
let result = cleanup(&non_existent, false, false, false, false, false);
assert!(
result.is_err(),
"cleanup() should fail on non-existent path, proving it calls the C library"
);
let fixture = SvnTestFixture::new();
let result = cleanup(&fixture.wc_path, false, false, false, false, false);
assert!(
result.is_ok(),
"cleanup() should succeed on valid working copy"
);
let result = cleanup(&fixture.wc_path, true, true, true, true, false);
assert!(
result.is_ok(),
"cleanup() with all options should succeed on valid working copy"
);
}
#[test]
fn test_context_check_wc_returns_format_number() {
let fixture = SvnTestFixture::new();
let mut wc_ctx = Context::new().unwrap();
let format_num = wc_ctx.check_wc(fixture.wc_path_str()).unwrap();
assert!(
format_num > 10,
"Working copy format should be > 10 for modern SVN, got {}",
format_num
);
assert!(
format_num < 100,
"Working copy format should be reasonable (< 100), got {}",
format_num
);
}
#[test]
fn test_free_function_check_wc() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_wc = temp_dir.path().join("not_a_wc");
std::fs::create_dir(&non_wc).unwrap();
let result = check_wc(&non_wc);
assert!(
result.is_ok(),
"check_wc should succeed on non-WC directory"
);
assert_eq!(
result.unwrap(),
None,
"check_wc should return None for non-WC directory"
);
let fixture = SvnTestFixture::new();
let result = check_wc(&fixture.wc_path);
assert!(result.is_ok(), "check_wc should succeed on valid WC");
let format_opt = result.unwrap();
assert!(
format_opt.is_some(),
"check_wc should return Some for valid WC, got None"
);
let format_num = format_opt.unwrap();
assert!(
format_num > 10,
"WC format should be > 10 for modern SVN, got {}",
format_num
);
assert!(
format_num < 100,
"WC format should be reasonable (< 100), got {}",
format_num
);
}
#[test]
fn test_upgrade_actually_executes() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_wc = temp_dir.path().join("not_a_wc");
std::fs::create_dir(&non_wc).unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.upgrade(non_wc.to_str().unwrap());
assert!(
result.is_err(),
"upgrade() should fail on non-WC directory, proving it calls the C library"
);
}
#[test]
fn test_prop_set_actually_executes() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("test.txt", "test content");
let mut wc_ctx = Context::new().unwrap();
assert_eq!(
wc_ctx.prop_get(&file_path, "test:prop").unwrap(),
None,
"Property should not exist before"
);
fixture
.client_ctx
.propset(
"test:prop",
Some(b"test value"),
file_path.to_str().expect("test path should be valid UTF-8"),
&crate::client::PropSetOptions::default(),
)
.unwrap();
assert_eq!(
wc_ctx.prop_get(&file_path, "test:prop").unwrap().as_deref(),
Some(&b"test value"[..]),
"client propset() should actually set the property"
);
}
#[test]
fn test_relocate_actually_executes() {
let mut fixture = SvnTestFixture::new();
let old_url = fixture.url.clone();
assert_eq!(fixture.get_wc_url(), old_url);
let (_repos_path2, new_url) = create_repo(fixture.temp_dir.path(), "repos2");
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.relocate(fixture.wc_path_str(), &old_url, &new_url)
.unwrap();
assert_eq!(
fixture.get_wc_url(),
new_url,
"relocate() should actually change the repository URL"
);
}
#[test]
fn test_add_lock_actually_executes() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_wc = temp_dir.path().join("not_a_wc");
std::fs::create_dir(&non_wc).unwrap();
let file_path = non_wc.join("file.txt");
std::fs::write(&file_path, "test").unwrap();
let mut wc_ctx = Context::new().unwrap();
let lock = Lock::from_ptr(std::ptr::null());
let result = wc_ctx.add_lock(&file_path, &lock);
assert!(
result.is_err(),
"add_lock() should fail on non-WC file, proving it calls the C library"
);
}
#[test]
fn test_remove_lock_actually_executes() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_wc = temp_dir.path().join("not_a_wc");
std::fs::create_dir(&non_wc).unwrap();
let file_path = non_wc.join("file.txt");
std::fs::write(&file_path, "test").unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.remove_lock(&file_path);
assert!(
result.is_err(),
"remove_lock() should fail on non-WC file, proving it calls the C library"
);
}
#[test]
fn test_ensure_adm_actually_executes() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let wc_path = temp_dir.path().join("new_wc");
std::fs::create_dir(&wc_path).unwrap();
let svn_dir = wc_path.join(".svn");
assert!(!svn_dir.exists(), ".svn should not exist before ensure_adm");
let result = ensure_adm(
&wc_path,
"test-uuid",
"file:///tmp/test-repo",
"file:///tmp/test-repo",
1,
);
assert!(result.is_ok(), "ensure_adm() should succeed: {:?}", result);
assert!(
svn_dir.exists(),
"ensure_adm() should create .svn directory"
);
}
#[test]
fn test_process_committed_queue_actually_executes() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let non_wc = temp_dir.path().join("not_a_wc");
std::fs::create_dir(&non_wc).unwrap();
let mut wc_ctx = Context::new().unwrap();
let mut queue = CommittedQueue::new();
let result = wc_ctx.process_committed_queue(
&mut queue,
crate::Revnum(1),
Some("2024-01-01T00:00:00.000000Z"),
Some("author"),
);
assert!(
result.is_ok(),
"process_committed_queue() should succeed on empty queue"
);
}
struct RecordingDiffCallbacks {
changed_files: Vec<String>,
added_files: Vec<String>,
deleted_files: Vec<String>,
}
impl RecordingDiffCallbacks {
fn new() -> Self {
Self {
changed_files: Vec::new(),
added_files: Vec::new(),
deleted_files: Vec::new(),
}
}
}
impl DiffCallbacks for RecordingDiffCallbacks {
fn file_opened(
&mut self,
_path: &str,
_rev: crate::Revnum,
) -> Result<(bool, bool), crate::Error<'static>> {
Ok((false, false))
}
fn file_changed(
&mut self,
change: &FileChange<'_>,
) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>> {
self.changed_files.push(change.path.to_string());
Ok((NotifyState::Changed, NotifyState::Unchanged, false))
}
fn file_added(
&mut self,
change: &FileChange<'_>,
_copyfrom_path: Option<&str>,
_copyfrom_revision: crate::Revnum,
) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>> {
self.added_files.push(change.path.to_string());
Ok((NotifyState::Changed, NotifyState::Unchanged, false))
}
fn file_deleted(
&mut self,
path: &str,
_tmpfile1: Option<&str>,
_tmpfile2: Option<&str>,
_mimetype1: Option<&str>,
_mimetype2: Option<&str>,
) -> Result<(NotifyState, bool), crate::Error<'static>> {
self.deleted_files.push(path.to_string());
Ok((NotifyState::Changed, false))
}
fn dir_deleted(
&mut self,
_path: &str,
) -> Result<(NotifyState, bool), crate::Error<'static>> {
Ok((NotifyState::Unchanged, false))
}
fn dir_opened(
&mut self,
_path: &str,
_rev: crate::Revnum,
) -> Result<(bool, bool, bool), crate::Error<'static>> {
Ok((false, false, false))
}
fn dir_added(
&mut self,
_path: &str,
_rev: crate::Revnum,
_copyfrom_path: Option<&str>,
_copyfrom_revision: crate::Revnum,
) -> Result<(NotifyState, bool, bool, bool), crate::Error<'static>> {
Ok((NotifyState::Changed, false, false, false))
}
fn dir_props_changed(
&mut self,
_path: &str,
_dir_was_added: bool,
_prop_changes: &[PropChange],
) -> Result<(NotifyState, bool), crate::Error<'static>> {
Ok((NotifyState::Unchanged, false))
}
fn dir_closed(
&mut self,
_path: &str,
_dir_was_added: bool,
) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>> {
Ok((NotifyState::Unchanged, NotifyState::Unchanged, false))
}
}
#[test]
fn test_diff_reports_modified_file() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("test.txt", "original content\n");
fixture.commit();
std::fs::write(fixture.wc_path.join("test.txt"), "modified content\n").unwrap();
let mut callbacks = RecordingDiffCallbacks::new();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.diff(&fixture.wc_path, &DiffOptions::default(), &mut callbacks)
.expect("diff() should succeed on a working copy with local modifications");
assert_eq!(
callbacks.changed_files,
vec!["test.txt"],
"only the modified file should be reported as changed"
);
assert!(
callbacks.added_files.is_empty(),
"no files should be reported as added"
);
assert!(
callbacks.deleted_files.is_empty(),
"no files should be reported as deleted"
);
}
#[test]
fn test_diff_reports_added_file() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("existing.txt", "exists\n");
fixture.commit();
fixture.add_file("new.txt", "brand new\n");
let mut callbacks = RecordingDiffCallbacks::new();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.diff(&fixture.wc_path, &DiffOptions::default(), &mut callbacks)
.expect("diff() should succeed");
assert_eq!(
callbacks.added_files,
vec!["new.txt"],
"the newly added file should be reported as added"
);
assert!(
callbacks.changed_files.is_empty(),
"no files should be reported as changed"
);
}
#[test]
fn test_diff_clean_working_copy_reports_nothing() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("clean.txt", "clean content\n");
fixture.commit();
let mut callbacks = RecordingDiffCallbacks::new();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.diff(&fixture.wc_path, &DiffOptions::default(), &mut callbacks)
.expect("diff() should succeed on a clean working copy");
assert!(
callbacks.changed_files.is_empty(),
"no changed files in a clean WC"
);
assert!(
callbacks.added_files.is_empty(),
"no added files in a clean WC"
);
assert!(
callbacks.deleted_files.is_empty(),
"no deleted files in a clean WC"
);
}
#[test]
fn test_merge_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
let target_path = fixture.add_file("target.txt", "line 1\nline 2\nline 3\n");
fixture.commit();
let left_path = fixture.temp_dir.path().join("left.txt");
let right_path = fixture.temp_dir.path().join("right.txt");
std::fs::write(&left_path, "line 1\nline 2\nline 3\n").unwrap();
std::fs::write(&right_path, "line 1\nline 2 modified\nline 3\n").unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.merge(
&left_path,
&right_path,
&target_path,
Some(".left"),
Some(".right"),
Some(".working"),
&[],
&MergeOptions::default(),
);
assert!(
result.is_err(),
"merge() must fail without a write lock; a mutation returning Ok(()) would be caught here"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"expected a write-lock error, got: {}",
err_msg
);
}
#[test]
fn test_merge_with_prop_diff_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
let target_path = fixture.add_file("target.txt", "content\n");
fixture.commit();
let left_path = fixture.temp_dir.path().join("left.txt");
let right_path = fixture.temp_dir.path().join("right.txt");
std::fs::write(&left_path, "content\n").unwrap();
std::fs::write(&right_path, "content modified\n").unwrap();
let prop_changes = vec![PropChange {
name: "svn:eol-style".to_string(),
value: Some(b"native".to_vec()),
}];
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.merge(
&left_path,
&right_path,
&target_path,
None,
None,
None,
&prop_changes,
&MergeOptions::default(),
);
assert!(
result.is_err(),
"merge() with prop_diff must fail without a write lock"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"expected a write-lock error, got: {}",
err_msg
);
}
#[test]
fn test_merge_props_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("propmerge.txt", "content\n");
fixture.commit();
let prop_changes = vec![PropChange {
name: "svn:keywords".to_string(),
value: Some(b"Id".to_vec()),
}];
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.merge_props(&file_path, None, &prop_changes, false, None, None);
assert!(
result.is_err(),
"merge_props() must fail without a write lock; a mutation returning Ok(()) would be caught here"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock")
|| err_msg.to_lowercase().contains("write")
|| err_msg.to_lowercase().contains("path"),
"expected a write-lock or path error, got: {}",
err_msg
);
}
#[test]
fn test_revert_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("revert_me.txt", "original\n");
fixture.commit();
std::fs::write(&file_path, "changed\n").unwrap();
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.revert(
&file_path,
&RevertOptions {
depth: crate::Depth::Empty,
..Default::default()
},
);
assert!(
result.is_err(),
"revert() must fail without a write lock; a mutation returning Ok(()) would be caught here"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"expected a write-lock error, got: {}",
err_msg
);
}
#[test]
fn test_revert_with_added_keep_local_false_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("existing.txt", "exists\n");
fixture.commit();
let new_file = fixture.add_file("new_file.txt", "new\n");
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.revert(
&new_file,
&RevertOptions {
depth: crate::Depth::Empty,
added_keep_local: false,
..Default::default()
},
);
assert!(
result.is_err(),
"revert() with added_keep_local=false must fail without a write lock"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"expected a write-lock error, got: {}",
err_msg
);
}
#[test]
fn test_revert_with_non_matching_changelist_is_noop() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("cl_file.txt", "content\n");
fixture.commit();
std::fs::write(&file_path, "modified\n").unwrap();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.revert(
&fixture.wc_path,
&RevertOptions {
depth: crate::Depth::Infinity,
changelists: vec!["no-such-changelist".to_string()],
..Default::default()
},
)
.expect("revert() with non-matching changelist should succeed as a no-op");
assert_eq!(
std::fs::read_to_string(&file_path).unwrap(),
"modified\n",
"file should be unchanged when changelist filter matches nothing"
);
}
#[test]
fn test_copy_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
let src_path = fixture.add_file("src.txt", "source content\n");
fixture.commit();
let dst_path = fixture.wc_path.join("dst.txt");
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.copy(&src_path, &dst_path, false);
assert!(
result.is_err(),
"copy() must fail without a write lock; a mutation returning Ok(()) would be caught here"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"expected a write-lock error, got: {}",
err_msg
);
}
#[test]
fn test_set_changelist_assigns_and_clears() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("hello.txt", "hello\n");
fixture.commit();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.set_changelist(&file_path, Some("my-cl"), crate::Depth::Empty, &[])
.expect("set_changelist() should succeed for a versioned file");
let filter = vec!["my-cl".to_string()];
let mut found: Vec<(String, Option<String>)> = Vec::new();
wc_ctx
.get_changelists(
&fixture.wc_path,
crate::Depth::Infinity,
&filter,
|path, cl| {
found.push((path.to_owned(), cl.map(str::to_owned)));
Ok(())
},
)
.expect("get_changelists() should succeed");
assert_eq!(found.len(), 1, "expected exactly one changelist entry");
assert_eq!(found[0].0, file_path.to_str().unwrap());
assert_eq!(found[0].1, Some("my-cl".to_owned()));
wc_ctx
.set_changelist(&file_path, None, crate::Depth::Empty, &[])
.expect("clearing changelist should succeed");
let mut after: Vec<(String, Option<String>)> = Vec::new();
wc_ctx
.get_changelists(
&fixture.wc_path,
crate::Depth::Infinity,
&filter,
|path, cl| {
after.push((path.to_owned(), cl.map(str::to_owned)));
Ok(())
},
)
.expect("get_changelists() after clear should succeed");
assert_eq!(after.len(), 0, "expected no changelist entries after clear");
}
#[test]
fn test_get_changelists_empty_wc() {
let fixture = SvnTestFixture::new();
let mut wc_ctx = Context::new().unwrap();
let mut with_changelist = 0usize;
wc_ctx
.get_changelists(
&fixture.wc_path,
crate::Depth::Infinity,
&[],
|_path, cl| {
if cl.is_some() {
with_changelist += 1;
}
Ok(())
},
)
.expect("get_changelists() on clean WC should succeed");
assert_eq!(
with_changelist, 0,
"expected zero nodes with a changelist in a fresh WC"
);
}
#[test]
fn test_get_changelists_filter() {
let mut fixture = SvnTestFixture::new();
let file_a = fixture.add_file("a.txt", "a\n");
let file_b = fixture.add_file("b.txt", "b\n");
fixture.commit();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.set_changelist(&file_a, Some("cl-alpha"), crate::Depth::Empty, &[])
.expect("set cl-alpha on a.txt");
wc_ctx
.set_changelist(&file_b, Some("cl-beta"), crate::Depth::Empty, &[])
.expect("set cl-beta on b.txt");
let filter = vec!["cl-alpha".to_string()];
let mut found: Vec<(String, Option<String>)> = Vec::new();
wc_ctx
.get_changelists(
&fixture.wc_path,
crate::Depth::Infinity,
&filter,
|path, cl| {
found.push((path.to_owned(), cl.map(str::to_owned)));
Ok(())
},
)
.expect("get_changelists() with filter should succeed");
assert_eq!(found.len(), 1, "expected only the cl-alpha entry");
assert_eq!(found[0].0, file_a.to_str().unwrap());
assert_eq!(found[0].1, Some("cl-alpha".to_owned()));
}
#[test]
fn test_status_unmodified_file() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("hello.txt", "hello\n");
fixture.commit();
let mut wc_ctx = Context::new().unwrap();
let status = wc_ctx
.status(&file_path)
.expect("status() should succeed for a versioned file");
assert_eq!(status.node_status(), StatusKind::Normal);
assert_eq!(status.text_status(), StatusKind::Normal);
}
#[test]
fn test_status_modified_file() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("hello.txt", "hello\n");
fixture.commit();
std::fs::write(&file_path, "modified\n").unwrap();
let mut wc_ctx = Context::new().unwrap();
let status = wc_ctx
.status(&file_path)
.expect("status() should succeed for a modified file");
assert_eq!(status.node_status(), StatusKind::Modified);
}
#[test]
fn test_status_dup() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("hello.txt", "hello\n");
fixture.commit();
std::fs::write(&file_path, "modified\n").unwrap();
let mut wc_ctx = Context::new().unwrap();
let status = wc_ctx.status(&file_path).expect("status() should succeed");
let duped = status.dup();
assert_eq!(duped.node_status(), status.node_status());
assert_eq!(duped.text_status(), status.text_status());
assert_eq!(duped.copied(), status.copied());
assert_eq!(duped.switched(), status.switched());
assert_eq!(duped.locked(), status.locked());
assert_eq!(duped.versioned(), status.versioned());
assert_eq!(duped.revision(), status.revision());
assert_eq!(duped.changed_rev(), status.changed_rev());
assert_eq!(duped.repos_relpath(), status.repos_relpath());
assert_eq!(duped.repos_uuid(), status.repos_uuid());
assert_eq!(duped.repos_root_url(), status.repos_root_url());
assert_eq!(duped.kind(), status.kind());
assert_eq!(duped.depth(), status.depth());
assert_eq!(duped.filesize(), status.filesize());
}
#[test]
fn test_check_root_wc_root() {
let fixture = SvnTestFixture::new();
let mut wc_ctx = Context::new().unwrap();
let (is_wcroot, is_switched, kind) = wc_ctx
.check_root(&fixture.wc_path)
.expect("check_root() should succeed on the WC root");
assert!(is_wcroot, "WC root directory should be reported as wcroot");
assert!(!is_switched, "fresh checkout should not be switched");
assert_eq!(kind, crate::NodeKind::Dir);
}
#[test]
fn test_check_root_non_root_file() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("hello.txt", "hello\n");
fixture.commit();
let mut wc_ctx = Context::new().unwrap();
let (is_wcroot, _is_switched, kind) = wc_ctx
.check_root(&file_path)
.expect("check_root() should succeed on a versioned file");
assert!(!is_wcroot, "a file is not a wcroot");
assert_eq!(kind, crate::NodeKind::File);
}
#[test]
fn test_restore_missing_file() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("hello.txt", "hello\n");
fixture.commit();
std::fs::remove_file(&file_path).unwrap();
let mut wc_ctx = Context::new().unwrap();
wc_ctx
.restore(&file_path, false)
.expect("restore() should succeed for a missing versioned file");
assert!(
file_path.exists(),
"file should be restored on disk after restore()"
);
assert_eq!(
std::fs::read_to_string(&file_path).unwrap(),
"hello\n",
"restored file should have original content"
);
}
#[test]
fn test_get_ignores_returns_patterns() {
let fixture = SvnTestFixture::new();
let mut wc_ctx = Context::new().unwrap();
let patterns = wc_ctx
.get_ignores(&fixture.wc_path)
.expect("get_ignores() should succeed on a valid WC directory");
assert!(
!patterns.is_empty(),
"expected at least some default ignore patterns"
);
assert!(
patterns.iter().any(|p| p == "*.o"),
"expected '*.o' in default ignore patterns, got: {:?}",
patterns
);
}
#[test]
fn test_get_ignores_includes_svn_ignore_property() {
let mut fixture = SvnTestFixture::new();
fixture.commit();
let wc_path_str = fixture.wc_path_str().to_owned();
fixture
.client_ctx
.propset(
"svn:ignore",
Some(b"my-custom-pattern\n"),
&wc_path_str,
&crate::client::PropSetOptions::default(),
)
.expect("propset svn:ignore should succeed");
let mut wc_ctx = Context::new().unwrap();
let patterns = wc_ctx
.get_ignores(&fixture.wc_path)
.expect("get_ignores() should succeed");
assert!(
patterns.iter().any(|p| p == "my-custom-pattern"),
"expected custom pattern in ignore list, got: {:?}",
patterns
);
}
#[test]
fn test_canonicalize_svn_prop_executable() {
let result = canonicalize_svn_prop(
"svn:executable",
b"yes",
"/some/path",
crate::NodeKind::File,
)
.expect("canonicalize should succeed for svn:executable");
assert_eq!(result, b"*");
}
#[test]
fn test_canonicalize_svn_prop_ignore_adds_newline() {
let result = canonicalize_svn_prop("svn:ignore", b"*.o", "/some/dir", crate::NodeKind::Dir)
.expect("canonicalize should succeed for svn:ignore");
assert_eq!(result, b"*.o\n");
}
#[test]
fn test_canonicalize_svn_prop_keywords_strips_whitespace() {
let result = canonicalize_svn_prop(
"svn:keywords",
b" Rev Author ",
"/some/path",
crate::NodeKind::File,
)
.expect("canonicalize should succeed for svn:keywords");
assert_eq!(result, b"Rev Author");
}
#[test]
fn test_canonicalize_svn_prop_invalid_prop_errors() {
let result = canonicalize_svn_prop(
"svn:ignore",
b"*.o\n",
"/some/path",
crate::NodeKind::File, );
assert!(
result.is_err(),
"svn:ignore on a file should produce a validation error"
);
}
#[test]
fn test_get_default_ignores_returns_patterns() {
let patterns = get_default_ignores().expect("get_default_ignores() should succeed");
assert!(
!patterns.is_empty(),
"expected at least some default ignore patterns"
);
assert!(
patterns.iter().any(|p| p == "*.o"),
"expected '*.o' in default ignore patterns, got: {:?}",
patterns
);
}
#[test]
fn test_get_default_ignores_does_not_include_svn_ignore() {
let mut fixture = SvnTestFixture::new();
fixture.commit();
let wc_path_str = fixture.wc_path_str().to_owned();
fixture
.client_ctx
.propset(
"svn:ignore",
Some(b"my-custom-pattern\n"),
&wc_path_str,
&crate::client::PropSetOptions::default(),
)
.expect("propset svn:ignore should succeed");
let patterns = get_default_ignores().expect("get_default_ignores() should succeed");
assert!(
!patterns.iter().any(|p| p == "my-custom-pattern"),
"get_default_ignores() must not include svn:ignore property values"
);
}
#[test]
fn test_remove_from_revision_control_requires_write_lock() {
let mut fixture = SvnTestFixture::new();
let file_path = fixture.add_file("versioned.txt", "content\n");
fixture.commit();
let mut wc_ctx = Context::new().unwrap();
let result = wc_ctx.remove_from_revision_control(&file_path, false, false);
assert!(
result.is_err(),
"remove_from_revision_control() must fail without a write lock"
);
let err_msg = format!("{:?}", result.unwrap_err());
assert!(
err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
"expected a write-lock error, got: {}",
err_msg
);
}
#[test]
fn test_lock_new_with_path_and_token() {
let lock = Lock::new(Some("/trunk/file.txt"), Some(b"opaquelocktoken:abc123"));
assert_eq!(lock.path(), Some("/trunk/file.txt"));
assert_eq!(lock.token(), Some("opaquelocktoken:abc123"));
}
#[test]
fn test_lock_new_with_none() {
let lock = Lock::new(None, None);
assert_eq!(lock.path(), None);
assert_eq!(lock.token(), None);
assert_eq!(lock.owner(), None);
assert_eq!(lock.comment(), None);
}
#[test]
fn test_lock_new_with_path_only() {
let lock = Lock::new(Some("/trunk/file.txt"), None);
assert_eq!(lock.path(), Some("/trunk/file.txt"));
assert_eq!(lock.token(), None);
}
#[test]
#[allow(deprecated)]
fn test_adm_relocate_without_validator() {
let fixture = SvnTestFixture::new();
let old_url = fixture.url.clone();
let (_repos_path2, new_url) = create_repo(fixture.temp_dir.path(), "repos2");
let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
let result = adm.relocate(fixture.wc_path_str(), &old_url, &new_url, true, None);
assert!(
result.is_ok(),
"Adm::relocate() with no validator should succeed: {:?}",
result.err()
);
}
#[test]
#[allow(deprecated)]
fn test_adm_relocate_with_validator() {
let fixture = SvnTestFixture::new();
let old_url = fixture.url.clone();
let (_repos_path2, new_url) = create_repo(fixture.temp_dir.path(), "repos2");
let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
let validator = |_uuid: &str,
_url: &str,
_root_url: &str|
-> Result<(), crate::Error<'static>> { Ok(()) };
let result = adm.relocate(
fixture.wc_path_str(),
&old_url,
&new_url,
true,
Some(&validator),
);
assert!(
result.is_ok(),
"Adm::relocate() with validator should succeed: {:?}",
result.err()
);
}
#[test]
#[allow(deprecated)]
fn test_adm_close() {
let fixture = SvnTestFixture::new();
let mut adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
adm.close();
}
#[test]
#[allow(deprecated)]
fn test_adm_close_idempotent() {
let fixture = SvnTestFixture::new();
let mut adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
adm.close();
adm.close(); }
#[test]
#[allow(deprecated)]
fn test_adm_queue_committed() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("test.txt", "hello");
fixture.commit();
let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
let mut queue = CommittedQueue::new();
let file_path = fixture.wc_path.join("test.txt");
let result = adm.queue_committed(
file_path.to_str().unwrap(),
&mut queue,
false,
false,
false,
None,
);
assert!(result.is_ok(), "queue_committed failed: {:?}", result.err());
}
#[test]
#[allow(deprecated)]
fn test_adm_queue_committed_with_digest() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("test.txt", "hello");
fixture.commit();
let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
let mut queue = CommittedQueue::new();
let digest: [u8; 16] = [0; 16];
let file_path = fixture.wc_path.join("test.txt");
let result = adm.queue_committed(
file_path.to_str().unwrap(),
&mut queue,
false,
false,
false,
Some(&digest),
);
assert!(
result.is_ok(),
"queue_committed with digest failed: {:?}",
result.err()
);
}
#[test]
#[allow(deprecated)]
fn test_adm_process_committed_queue() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("test.txt", "hello");
fixture.commit();
let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
let mut queue = CommittedQueue::new();
let file_path = fixture.wc_path.join("test.txt");
adm.queue_committed(
file_path.to_str().unwrap(),
&mut queue,
false,
false,
false,
None,
)
.unwrap();
let result = adm.process_committed_queue(
&mut queue,
crate::Revnum(2),
Some("2026-03-24T00:00:00.000000Z"),
Some("testuser"),
);
assert!(
result.is_ok(),
"process_committed_queue failed: {:?}",
result.err()
);
}
#[test]
#[allow(deprecated)]
fn test_adm_process_committed_queue_no_date_or_author() {
let mut fixture = SvnTestFixture::new();
fixture.add_file("test.txt", "hello");
fixture.commit();
let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
let mut queue = CommittedQueue::new();
let file_path = fixture.wc_path.join("test.txt");
adm.queue_committed(
file_path.to_str().unwrap(),
&mut queue,
false,
false,
false,
None,
)
.unwrap();
let result = adm.process_committed_queue(&mut queue, crate::Revnum(2), None, None);
assert!(
result.is_ok(),
"process_committed_queue with no date/author failed: {:?}",
result.err()
);
}
}