1use crate::{svn_result, with_tmp_pool, Error};
35use std::marker::PhantomData;
36use subversion_sys::{svn_wc_context_t, svn_wc_version};
37
38fn box_cancel_baton(f: Box<dyn Fn() -> Result<(), Error<'static>>>) -> *mut std::ffi::c_void {
42 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
43}
44
45fn box_notify_baton(f: Box<dyn Fn(&Notify)>) -> *mut std::ffi::c_void {
46 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
47}
48
49fn box_conflict_baton(
50 f: Box<
51 dyn Fn(
52 &crate::conflict::ConflictDescription,
53 ) -> Result<crate::conflict::ConflictResult, Error<'static>>,
54 >,
55) -> *mut std::ffi::c_void {
56 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
57}
58
59fn box_external_baton(
60 f: Box<dyn Fn(&str, Option<&str>, Option<&str>, crate::Depth) -> Result<(), Error<'static>>>,
61) -> *mut std::ffi::c_void {
62 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
63}
64
65fn box_fetch_dirents_baton(
66 f: Box<
67 dyn Fn(
68 &str,
69 &str,
70 )
71 -> Result<std::collections::HashMap<String, crate::DirEntry>, Error<'static>>,
72 >,
73) -> *mut std::ffi::c_void {
74 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
75}
76
77fn box_cancel_baton_borrowed(f: &dyn Fn() -> Result<(), Error<'static>>) -> *mut std::ffi::c_void {
79 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
80}
81
82pub(crate) fn box_notify_baton_borrowed(f: &dyn Fn(&Notify)) -> *mut std::ffi::c_void {
83 Box::into_raw(Box::new(f)) as *mut std::ffi::c_void
84}
85
86unsafe fn drop_cancel_baton(baton: *mut std::ffi::c_void) {
88 drop(Box::from_raw(
89 baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
90 ));
91}
92
93unsafe fn drop_notify_baton(baton: *mut std::ffi::c_void) {
94 drop(Box::from_raw(baton as *mut Box<dyn Fn(&Notify)>));
95}
96
97unsafe fn drop_conflict_baton(baton: *mut std::ffi::c_void) {
98 drop(Box::from_raw(
99 baton
100 as *mut Box<
101 dyn Fn(
102 &crate::conflict::ConflictDescription,
103 ) -> Result<crate::conflict::ConflictResult, Error<'static>>,
104 >,
105 ));
106}
107
108unsafe fn drop_external_baton(baton: *mut std::ffi::c_void) {
109 drop(Box::from_raw(
110 baton
111 as *mut Box<
112 dyn Fn(
113 &str,
114 Option<&str>,
115 Option<&str>,
116 crate::Depth,
117 ) -> Result<(), Error<'static>>,
118 >,
119 ));
120}
121
122unsafe fn drop_fetch_dirents_baton(baton: *mut std::ffi::c_void) {
123 drop(Box::from_raw(
124 baton
125 as *mut Box<
126 dyn Fn(
127 &str,
128 &str,
129 ) -> Result<
130 std::collections::HashMap<String, crate::DirEntry>,
131 Error<'static>,
132 >,
133 >,
134 ));
135}
136
137unsafe fn drop_cancel_baton_borrowed(baton: *mut std::ffi::c_void) {
139 drop(Box::from_raw(
140 baton as *mut &dyn Fn() -> Result<(), Error<'static>>,
141 ));
142}
143
144pub(crate) unsafe fn drop_notify_baton_borrowed(baton: *mut std::ffi::c_void) {
145 drop(Box::from_raw(baton as *mut &dyn Fn(&Notify)));
146}
147
148pub fn version() -> crate::Version {
150 unsafe { crate::Version(svn_wc_version()) }
151}
152
153pub const STATUS_NONE: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_none as u32;
156pub const STATUS_UNVERSIONED: u32 =
158 subversion_sys::svn_wc_status_kind_svn_wc_status_unversioned as u32;
159pub const STATUS_NORMAL: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_normal as u32;
161pub const STATUS_ADDED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_added as u32;
163pub const STATUS_MISSING: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_missing as u32;
165pub const STATUS_DELETED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_deleted as u32;
167pub const STATUS_REPLACED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_replaced as u32;
169pub const STATUS_MODIFIED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_modified as u32;
171pub const STATUS_MERGED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_merged as u32;
173pub const STATUS_CONFLICTED: u32 =
175 subversion_sys::svn_wc_status_kind_svn_wc_status_conflicted as u32;
176pub const STATUS_IGNORED: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_ignored as u32;
178pub const STATUS_OBSTRUCTED: u32 =
180 subversion_sys::svn_wc_status_kind_svn_wc_status_obstructed as u32;
181pub const STATUS_EXTERNAL: u32 = subversion_sys::svn_wc_status_kind_svn_wc_status_external as u32;
183pub const STATUS_INCOMPLETE: u32 =
185 subversion_sys::svn_wc_status_kind_svn_wc_status_incomplete as u32;
186
187pub const SCHEDULE_NORMAL: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal as u32;
190pub const SCHEDULE_ADD: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add as u32;
192pub const SCHEDULE_DELETE: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete as u32;
194pub const SCHEDULE_REPLACE: u32 = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_replace as u32;
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199#[repr(u32)]
200pub enum StatusKind {
201 None = subversion_sys::svn_wc_status_kind_svn_wc_status_none as u32,
203 Unversioned = subversion_sys::svn_wc_status_kind_svn_wc_status_unversioned as u32,
205 Normal = subversion_sys::svn_wc_status_kind_svn_wc_status_normal as u32,
207 Added = subversion_sys::svn_wc_status_kind_svn_wc_status_added as u32,
209 Missing = subversion_sys::svn_wc_status_kind_svn_wc_status_missing as u32,
211 Deleted = subversion_sys::svn_wc_status_kind_svn_wc_status_deleted as u32,
213 Replaced = subversion_sys::svn_wc_status_kind_svn_wc_status_replaced as u32,
215 Modified = subversion_sys::svn_wc_status_kind_svn_wc_status_modified as u32,
217 Merged = subversion_sys::svn_wc_status_kind_svn_wc_status_merged as u32,
219 Conflicted = subversion_sys::svn_wc_status_kind_svn_wc_status_conflicted as u32,
221 Ignored = subversion_sys::svn_wc_status_kind_svn_wc_status_ignored as u32,
223 Obstructed = subversion_sys::svn_wc_status_kind_svn_wc_status_obstructed as u32,
225 External = subversion_sys::svn_wc_status_kind_svn_wc_status_external as u32,
227 Incomplete = subversion_sys::svn_wc_status_kind_svn_wc_status_incomplete as u32,
229}
230
231impl From<subversion_sys::svn_wc_status_kind> for StatusKind {
232 fn from(status: subversion_sys::svn_wc_status_kind) -> Self {
233 match status {
234 subversion_sys::svn_wc_status_kind_svn_wc_status_none => StatusKind::None,
235 subversion_sys::svn_wc_status_kind_svn_wc_status_unversioned => StatusKind::Unversioned,
236 subversion_sys::svn_wc_status_kind_svn_wc_status_normal => StatusKind::Normal,
237 subversion_sys::svn_wc_status_kind_svn_wc_status_added => StatusKind::Added,
238 subversion_sys::svn_wc_status_kind_svn_wc_status_missing => StatusKind::Missing,
239 subversion_sys::svn_wc_status_kind_svn_wc_status_deleted => StatusKind::Deleted,
240 subversion_sys::svn_wc_status_kind_svn_wc_status_replaced => StatusKind::Replaced,
241 subversion_sys::svn_wc_status_kind_svn_wc_status_modified => StatusKind::Modified,
242 subversion_sys::svn_wc_status_kind_svn_wc_status_merged => StatusKind::Merged,
243 subversion_sys::svn_wc_status_kind_svn_wc_status_conflicted => StatusKind::Conflicted,
244 subversion_sys::svn_wc_status_kind_svn_wc_status_ignored => StatusKind::Ignored,
245 subversion_sys::svn_wc_status_kind_svn_wc_status_obstructed => StatusKind::Obstructed,
246 subversion_sys::svn_wc_status_kind_svn_wc_status_external => StatusKind::External,
247 subversion_sys::svn_wc_status_kind_svn_wc_status_incomplete => StatusKind::Incomplete,
248 _ => unreachable!("unknown svn_wc_status_kind value: {}", status),
249 }
250 }
251}
252
253#[derive(Debug, Clone, PartialEq, Eq)]
258pub struct PropChange {
259 pub name: String,
261 pub value: Option<Vec<u8>>,
263}
264
265pub struct Status<'pool> {
267 ptr: *const subversion_sys::svn_wc_status3_t,
268 _pool: apr::pool::PoolHandle<'pool>,
273}
274
275impl<'pool> Status<'pool> {
276 pub fn node_status(&self) -> StatusKind {
278 unsafe { (*self.ptr).node_status.into() }
279 }
280
281 pub fn text_status(&self) -> StatusKind {
283 unsafe { (*self.ptr).text_status.into() }
284 }
285
286 pub fn prop_status(&self) -> StatusKind {
288 unsafe { (*self.ptr).prop_status.into() }
289 }
290
291 pub fn copied(&self) -> bool {
293 unsafe { (*self.ptr).copied != 0 }
294 }
295
296 pub fn switched(&self) -> bool {
298 unsafe { (*self.ptr).switched != 0 }
299 }
300
301 pub fn locked(&self) -> bool {
303 unsafe { (*self.ptr).locked != 0 }
304 }
305
306 pub fn revision(&self) -> crate::Revnum {
308 unsafe { crate::Revnum((*self.ptr).revision) }
309 }
310
311 pub fn changed_rev(&self) -> crate::Revnum {
313 unsafe { crate::Revnum((*self.ptr).changed_rev) }
314 }
315
316 pub fn repos_relpath(&self) -> Option<String> {
318 unsafe {
319 if (*self.ptr).repos_relpath.is_null() {
320 None
321 } else {
322 Some(
323 std::ffi::CStr::from_ptr((*self.ptr).repos_relpath)
324 .to_string_lossy()
325 .into_owned(),
326 )
327 }
328 }
329 }
330
331 pub fn kind(&self) -> i32 {
333 unsafe { (*self.ptr).kind as i32 }
334 }
335
336 pub fn depth(&self) -> i32 {
338 unsafe { (*self.ptr).depth }
339 }
340
341 pub fn filesize(&self) -> i64 {
343 unsafe { (*self.ptr).filesize }
344 }
345
346 pub fn versioned(&self) -> bool {
348 unsafe { (*self.ptr).versioned != 0 }
349 }
350
351 pub fn repos_uuid(&self) -> Option<String> {
353 unsafe {
354 if (*self.ptr).repos_uuid.is_null() {
355 None
356 } else {
357 Some(
358 std::ffi::CStr::from_ptr((*self.ptr).repos_uuid)
359 .to_string_lossy()
360 .into_owned(),
361 )
362 }
363 }
364 }
365
366 pub fn repos_root_url(&self) -> Option<String> {
368 unsafe {
369 if (*self.ptr).repos_root_url.is_null() {
370 None
371 } else {
372 Some(
373 std::ffi::CStr::from_ptr((*self.ptr).repos_root_url)
374 .to_string_lossy()
375 .into_owned(),
376 )
377 }
378 }
379 }
380
381 pub fn dup(&self) -> Status<'static> {
383 let pool = apr::pool::Pool::new();
384 let ptr = unsafe { subversion_sys::svn_wc_dup_status3(self.ptr, pool.as_mut_ptr()) };
385 Status {
386 ptr,
387 _pool: apr::pool::PoolHandle::Owned(pool),
388 }
389 }
390}
391
392#[derive(Debug, Clone, Copy, PartialEq, Eq)]
394#[repr(u32)]
395pub enum Schedule {
396 Normal = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal as u32,
398 Add = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add as u32,
400 Delete = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete as u32,
402 Replace = subversion_sys::svn_wc_schedule_t_svn_wc_schedule_replace as u32,
404}
405
406impl From<subversion_sys::svn_wc_schedule_t> for Schedule {
407 fn from(schedule: subversion_sys::svn_wc_schedule_t) -> Self {
408 match schedule {
409 subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal => Schedule::Normal,
410 subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add => Schedule::Add,
411 subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete => Schedule::Delete,
412 subversion_sys::svn_wc_schedule_t_svn_wc_schedule_replace => Schedule::Replace,
413 _ => unreachable!("unknown svn_wc_schedule_t value: {}", schedule),
414 }
415 }
416}
417
418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
420pub enum MergeOutcome {
421 Unchanged,
423 Merged,
425 Conflict,
427 NoMerge,
429}
430
431impl From<subversion_sys::svn_wc_merge_outcome_t> for MergeOutcome {
432 fn from(outcome: subversion_sys::svn_wc_merge_outcome_t) -> Self {
433 match outcome {
434 subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_unchanged => {
435 MergeOutcome::Unchanged
436 }
437 subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_merged => MergeOutcome::Merged,
438 subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_conflict => MergeOutcome::Conflict,
439 subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_no_merge => MergeOutcome::NoMerge,
440 _ => unreachable!("unknown svn_wc_merge_outcome_t value: {}", outcome),
441 }
442 }
443}
444
445#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum NotifyState {
448 Inapplicable,
450 Unknown,
452 Unchanged,
454 Missing,
456 Obstructed,
458 Changed,
460 Merged,
462 Conflicted,
464 SourceMissing,
466}
467
468impl From<subversion_sys::svn_wc_notify_state_t> for NotifyState {
469 fn from(state: subversion_sys::svn_wc_notify_state_t) -> Self {
470 match state {
471 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable => {
472 NotifyState::Inapplicable
473 }
474 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unknown => {
475 NotifyState::Unknown
476 }
477 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unchanged => {
478 NotifyState::Unchanged
479 }
480 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_missing => {
481 NotifyState::Missing
482 }
483 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_obstructed => {
484 NotifyState::Obstructed
485 }
486 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_changed => {
487 NotifyState::Changed
488 }
489 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_merged => NotifyState::Merged,
490 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_conflicted => {
491 NotifyState::Conflicted
492 }
493 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_source_missing => {
494 NotifyState::SourceMissing
495 }
496 _ => unreachable!("unknown svn_wc_notify_state_t value: {}", state),
497 }
498 }
499}
500
501impl From<NotifyState> for subversion_sys::svn_wc_notify_state_t {
502 fn from(state: NotifyState) -> Self {
503 match state {
504 NotifyState::Inapplicable => {
505 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable
506 }
507 NotifyState::Unknown => {
508 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unknown
509 }
510 NotifyState::Unchanged => {
511 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_unchanged
512 }
513 NotifyState::Missing => {
514 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_missing
515 }
516 NotifyState::Obstructed => {
517 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_obstructed
518 }
519 NotifyState::Changed => {
520 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_changed
521 }
522 NotifyState::Merged => subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_merged,
523 NotifyState::Conflicted => {
524 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_conflicted
525 }
526 NotifyState::SourceMissing => {
527 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_source_missing
528 }
529 }
530 }
531}
532
533pub struct FileChange<'a> {
535 pub path: &'a str,
537 pub tmpfile1: Option<&'a str>,
539 pub tmpfile2: Option<&'a str>,
541 pub rev1: crate::Revnum,
543 pub rev2: crate::Revnum,
545 pub mimetype1: Option<&'a str>,
547 pub mimetype2: Option<&'a str>,
549 pub prop_changes: Vec<PropChange>,
551}
552
553unsafe fn diff_prop_array_to_vec(arr: *const apr_sys::apr_array_header_t) -> Vec<PropChange> {
557 if arr.is_null() {
558 return Vec::new();
559 }
560 let typed = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::from_ptr(
561 arr as *mut apr_sys::apr_array_header_t,
562 );
563 typed
564 .iter()
565 .map(|p| {
566 let name = if p.name.is_null() {
567 String::new()
568 } else {
569 std::ffi::CStr::from_ptr(p.name)
570 .to_string_lossy()
571 .into_owned()
572 };
573 let value = if p.value.is_null() {
574 None
575 } else {
576 Some(crate::svn_string_helpers::to_vec(&*p.value))
577 };
578 PropChange { name, value }
579 })
580 .collect()
581}
582
583unsafe fn diff_opt_str<'a>(p: *const std::os::raw::c_char) -> Option<&'a str> {
585 if p.is_null() {
586 None
587 } else {
588 std::ffi::CStr::from_ptr(p).to_str().ok()
589 }
590}
591
592unsafe extern "C" fn diff_cb_file_opened(
593 tree_conflicted: *mut subversion_sys::svn_boolean_t,
594 skip: *mut subversion_sys::svn_boolean_t,
595 path: *const std::os::raw::c_char,
596 rev: subversion_sys::svn_revnum_t,
597 diff_baton: *mut std::ffi::c_void,
598 _scratch_pool: *mut apr_sys::apr_pool_t,
599) -> *mut subversion_sys::svn_error_t {
600 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
601 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
602 match cb.file_opened(path, crate::Revnum(rev)) {
603 Ok((tc, sk)) => {
604 if !tree_conflicted.is_null() {
605 *tree_conflicted = tc as i32;
606 }
607 if !skip.is_null() {
608 *skip = sk as i32;
609 }
610 std::ptr::null_mut()
611 }
612 Err(e) => e.into_raw(),
613 }
614}
615
616unsafe extern "C" fn diff_cb_file_changed(
617 contentstate: *mut subversion_sys::svn_wc_notify_state_t,
618 propstate: *mut subversion_sys::svn_wc_notify_state_t,
619 tree_conflicted: *mut subversion_sys::svn_boolean_t,
620 path: *const std::os::raw::c_char,
621 tmpfile1: *const std::os::raw::c_char,
622 tmpfile2: *const std::os::raw::c_char,
623 rev1: subversion_sys::svn_revnum_t,
624 rev2: subversion_sys::svn_revnum_t,
625 mimetype1: *const std::os::raw::c_char,
626 mimetype2: *const std::os::raw::c_char,
627 propchanges: *const apr_sys::apr_array_header_t,
628 _originalprops: *mut apr_sys::apr_hash_t,
629 diff_baton: *mut std::ffi::c_void,
630 _scratch_pool: *mut apr_sys::apr_pool_t,
631) -> *mut subversion_sys::svn_error_t {
632 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
633 let change = FileChange {
634 path: std::ffi::CStr::from_ptr(path).to_str().unwrap_or(""),
635 tmpfile1: diff_opt_str(tmpfile1),
636 tmpfile2: diff_opt_str(tmpfile2),
637 rev1: crate::Revnum(rev1),
638 rev2: crate::Revnum(rev2),
639 mimetype1: diff_opt_str(mimetype1),
640 mimetype2: diff_opt_str(mimetype2),
641 prop_changes: diff_prop_array_to_vec(propchanges),
642 };
643 match cb.file_changed(&change) {
644 Ok((cs, ps, tc)) => {
645 if !contentstate.is_null() {
646 *contentstate = cs.into();
647 }
648 if !propstate.is_null() {
649 *propstate = ps.into();
650 }
651 if !tree_conflicted.is_null() {
652 *tree_conflicted = tc as i32;
653 }
654 std::ptr::null_mut()
655 }
656 Err(e) => e.into_raw(),
657 }
658}
659
660unsafe extern "C" fn diff_cb_file_added(
661 contentstate: *mut subversion_sys::svn_wc_notify_state_t,
662 propstate: *mut subversion_sys::svn_wc_notify_state_t,
663 tree_conflicted: *mut subversion_sys::svn_boolean_t,
664 path: *const std::os::raw::c_char,
665 tmpfile1: *const std::os::raw::c_char,
666 tmpfile2: *const std::os::raw::c_char,
667 rev1: subversion_sys::svn_revnum_t,
668 rev2: subversion_sys::svn_revnum_t,
669 mimetype1: *const std::os::raw::c_char,
670 mimetype2: *const std::os::raw::c_char,
671 copyfrom_path: *const std::os::raw::c_char,
672 copyfrom_revision: subversion_sys::svn_revnum_t,
673 propchanges: *const apr_sys::apr_array_header_t,
674 _originalprops: *mut apr_sys::apr_hash_t,
675 diff_baton: *mut std::ffi::c_void,
676 _scratch_pool: *mut apr_sys::apr_pool_t,
677) -> *mut subversion_sys::svn_error_t {
678 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
679 let change = FileChange {
680 path: std::ffi::CStr::from_ptr(path).to_str().unwrap_or(""),
681 tmpfile1: diff_opt_str(tmpfile1),
682 tmpfile2: diff_opt_str(tmpfile2),
683 rev1: crate::Revnum(rev1),
684 rev2: crate::Revnum(rev2),
685 mimetype1: diff_opt_str(mimetype1),
686 mimetype2: diff_opt_str(mimetype2),
687 prop_changes: diff_prop_array_to_vec(propchanges),
688 };
689 match cb.file_added(
690 &change,
691 diff_opt_str(copyfrom_path),
692 crate::Revnum(copyfrom_revision),
693 ) {
694 Ok((cs, ps, tc)) => {
695 if !contentstate.is_null() {
696 *contentstate = cs.into();
697 }
698 if !propstate.is_null() {
699 *propstate = ps.into();
700 }
701 if !tree_conflicted.is_null() {
702 *tree_conflicted = tc as i32;
703 }
704 std::ptr::null_mut()
705 }
706 Err(e) => e.into_raw(),
707 }
708}
709
710unsafe extern "C" fn diff_cb_file_deleted(
711 state: *mut subversion_sys::svn_wc_notify_state_t,
712 tree_conflicted: *mut subversion_sys::svn_boolean_t,
713 path: *const std::os::raw::c_char,
714 tmpfile1: *const std::os::raw::c_char,
715 tmpfile2: *const std::os::raw::c_char,
716 mimetype1: *const std::os::raw::c_char,
717 mimetype2: *const std::os::raw::c_char,
718 _originalprops: *mut apr_sys::apr_hash_t,
719 diff_baton: *mut std::ffi::c_void,
720 _scratch_pool: *mut apr_sys::apr_pool_t,
721) -> *mut subversion_sys::svn_error_t {
722 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
723 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
724 match cb.file_deleted(
725 path,
726 diff_opt_str(tmpfile1),
727 diff_opt_str(tmpfile2),
728 diff_opt_str(mimetype1),
729 diff_opt_str(mimetype2),
730 ) {
731 Ok((st, tc)) => {
732 if !state.is_null() {
733 *state = st.into();
734 }
735 if !tree_conflicted.is_null() {
736 *tree_conflicted = tc as i32;
737 }
738 std::ptr::null_mut()
739 }
740 Err(e) => e.into_raw(),
741 }
742}
743
744unsafe extern "C" fn diff_cb_dir_deleted(
745 state: *mut subversion_sys::svn_wc_notify_state_t,
746 tree_conflicted: *mut subversion_sys::svn_boolean_t,
747 path: *const std::os::raw::c_char,
748 diff_baton: *mut std::ffi::c_void,
749 _scratch_pool: *mut apr_sys::apr_pool_t,
750) -> *mut subversion_sys::svn_error_t {
751 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
752 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
753 match cb.dir_deleted(path) {
754 Ok((st, tc)) => {
755 if !state.is_null() {
756 *state = st.into();
757 }
758 if !tree_conflicted.is_null() {
759 *tree_conflicted = tc as i32;
760 }
761 std::ptr::null_mut()
762 }
763 Err(e) => e.into_raw(),
764 }
765}
766
767unsafe extern "C" fn diff_cb_dir_opened(
768 tree_conflicted: *mut subversion_sys::svn_boolean_t,
769 skip: *mut subversion_sys::svn_boolean_t,
770 skip_children: *mut subversion_sys::svn_boolean_t,
771 path: *const std::os::raw::c_char,
772 rev: subversion_sys::svn_revnum_t,
773 diff_baton: *mut std::ffi::c_void,
774 _scratch_pool: *mut apr_sys::apr_pool_t,
775) -> *mut subversion_sys::svn_error_t {
776 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
777 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
778 match cb.dir_opened(path, crate::Revnum(rev)) {
779 Ok((tc, sk, skc)) => {
780 if !tree_conflicted.is_null() {
781 *tree_conflicted = tc as i32;
782 }
783 if !skip.is_null() {
784 *skip = sk as i32;
785 }
786 if !skip_children.is_null() {
787 *skip_children = skc as i32;
788 }
789 std::ptr::null_mut()
790 }
791 Err(e) => e.into_raw(),
792 }
793}
794
795unsafe extern "C" fn diff_cb_dir_added(
796 state: *mut subversion_sys::svn_wc_notify_state_t,
797 tree_conflicted: *mut subversion_sys::svn_boolean_t,
798 skip: *mut subversion_sys::svn_boolean_t,
799 skip_children: *mut subversion_sys::svn_boolean_t,
800 path: *const std::os::raw::c_char,
801 rev: subversion_sys::svn_revnum_t,
802 copyfrom_path: *const std::os::raw::c_char,
803 copyfrom_revision: subversion_sys::svn_revnum_t,
804 diff_baton: *mut std::ffi::c_void,
805 _scratch_pool: *mut apr_sys::apr_pool_t,
806) -> *mut subversion_sys::svn_error_t {
807 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
808 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
809 match cb.dir_added(
810 path,
811 crate::Revnum(rev),
812 diff_opt_str(copyfrom_path),
813 crate::Revnum(copyfrom_revision),
814 ) {
815 Ok((st, tc, sk, skc)) => {
816 if !state.is_null() {
817 *state = st.into();
818 }
819 if !tree_conflicted.is_null() {
820 *tree_conflicted = tc as i32;
821 }
822 if !skip.is_null() {
823 *skip = sk as i32;
824 }
825 if !skip_children.is_null() {
826 *skip_children = skc as i32;
827 }
828 std::ptr::null_mut()
829 }
830 Err(e) => e.into_raw(),
831 }
832}
833
834unsafe extern "C" fn diff_cb_dir_props_changed(
835 propstate: *mut subversion_sys::svn_wc_notify_state_t,
836 tree_conflicted: *mut subversion_sys::svn_boolean_t,
837 path: *const std::os::raw::c_char,
838 dir_was_added: subversion_sys::svn_boolean_t,
839 propchanges: *const apr_sys::apr_array_header_t,
840 _original_props: *mut apr_sys::apr_hash_t,
841 diff_baton: *mut std::ffi::c_void,
842 _scratch_pool: *mut apr_sys::apr_pool_t,
843) -> *mut subversion_sys::svn_error_t {
844 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
845 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
846 let changes = diff_prop_array_to_vec(propchanges);
847 match cb.dir_props_changed(path, dir_was_added != 0, &changes) {
848 Ok((ps, tc)) => {
849 if !propstate.is_null() {
850 *propstate = ps.into();
851 }
852 if !tree_conflicted.is_null() {
853 *tree_conflicted = tc as i32;
854 }
855 std::ptr::null_mut()
856 }
857 Err(e) => e.into_raw(),
858 }
859}
860
861unsafe extern "C" fn diff_cb_dir_closed(
862 contentstate: *mut subversion_sys::svn_wc_notify_state_t,
863 propstate: *mut subversion_sys::svn_wc_notify_state_t,
864 tree_conflicted: *mut subversion_sys::svn_boolean_t,
865 path: *const std::os::raw::c_char,
866 dir_was_added: subversion_sys::svn_boolean_t,
867 diff_baton: *mut std::ffi::c_void,
868 _scratch_pool: *mut apr_sys::apr_pool_t,
869) -> *mut subversion_sys::svn_error_t {
870 let cb = &mut *(diff_baton as *mut &mut dyn DiffCallbacks);
871 let path = std::ffi::CStr::from_ptr(path).to_str().unwrap_or("");
872 match cb.dir_closed(path, dir_was_added != 0) {
873 Ok((cs, ps, tc)) => {
874 if !contentstate.is_null() {
875 *contentstate = cs.into();
876 }
877 if !propstate.is_null() {
878 *propstate = ps.into();
879 }
880 if !tree_conflicted.is_null() {
881 *tree_conflicted = tc as i32;
882 }
883 std::ptr::null_mut()
884 }
885 Err(e) => e.into_raw(),
886 }
887}
888
889fn make_diff_callbacks4() -> subversion_sys::svn_wc_diff_callbacks4_t {
891 subversion_sys::svn_wc_diff_callbacks4_t {
892 file_opened: Some(diff_cb_file_opened),
893 file_changed: Some(diff_cb_file_changed),
894 file_added: Some(diff_cb_file_added),
895 file_deleted: Some(diff_cb_file_deleted),
896 dir_deleted: Some(diff_cb_dir_deleted),
897 dir_opened: Some(diff_cb_dir_opened),
898 dir_added: Some(diff_cb_dir_added),
899 dir_props_changed: Some(diff_cb_dir_props_changed),
900 dir_closed: Some(diff_cb_dir_closed),
901 }
902}
903
904pub trait DiffCallbacks {
909 fn file_opened(
914 &mut self,
915 path: &str,
916 rev: crate::Revnum,
917 ) -> Result<(bool, bool), crate::Error<'static>>;
918
919 fn file_changed(
923 &mut self,
924 change: &FileChange<'_>,
925 ) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>>;
926
927 fn file_added(
934 &mut self,
935 change: &FileChange<'_>,
936 copyfrom_path: Option<&str>,
937 copyfrom_revision: crate::Revnum,
938 ) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>>;
939
940 fn file_deleted(
944 &mut self,
945 path: &str,
946 tmpfile1: Option<&str>,
947 tmpfile2: Option<&str>,
948 mimetype1: Option<&str>,
949 mimetype2: Option<&str>,
950 ) -> Result<(NotifyState, bool), crate::Error<'static>>;
951
952 fn dir_deleted(&mut self, path: &str) -> Result<(NotifyState, bool), crate::Error<'static>>;
956
957 fn dir_opened(
962 &mut self,
963 path: &str,
964 rev: crate::Revnum,
965 ) -> Result<(bool, bool, bool), crate::Error<'static>>;
966
967 fn dir_added(
971 &mut self,
972 path: &str,
973 rev: crate::Revnum,
974 copyfrom_path: Option<&str>,
975 copyfrom_revision: crate::Revnum,
976 ) -> Result<(NotifyState, bool, bool, bool), crate::Error<'static>>;
977
978 fn dir_props_changed(
982 &mut self,
983 path: &str,
984 dir_was_added: bool,
985 prop_changes: &[PropChange],
986 ) -> Result<(NotifyState, bool), crate::Error<'static>>;
987
988 fn dir_closed(
992 &mut self,
993 path: &str,
994 dir_was_added: bool,
995 ) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>>;
996}
997
998pub struct DiffOptions {
1000 pub depth: crate::Depth,
1002 pub ignore_ancestry: bool,
1004 pub show_copies_as_adds: bool,
1006 pub use_git_diff_format: bool,
1008 pub changelists: Vec<String>,
1010}
1011
1012impl Default for DiffOptions {
1013 fn default() -> Self {
1014 Self {
1015 depth: crate::Depth::Infinity,
1016 ignore_ancestry: false,
1017 show_copies_as_adds: false,
1018 use_git_diff_format: false,
1019 changelists: Vec::new(),
1020 }
1021 }
1022}
1023
1024#[derive(Default)]
1026pub struct MergeOptions {
1027 pub dry_run: bool,
1029 pub diff3_cmd: Option<String>,
1031 pub merge_options: Vec<String>,
1033}
1034
1035pub struct RevertOptions {
1037 pub depth: crate::Depth,
1039 pub use_commit_times: bool,
1042 pub changelists: Vec<String>,
1044 pub clear_changelists: bool,
1046 pub metadata_only: bool,
1049 pub added_keep_local: bool,
1052}
1053
1054impl Default for RevertOptions {
1055 fn default() -> Self {
1056 Self {
1057 depth: crate::Depth::Empty,
1058 use_commit_times: false,
1059 changelists: Vec::new(),
1060 clear_changelists: false,
1061 metadata_only: false,
1062 added_keep_local: true,
1063 }
1064 }
1065}
1066
1067pub struct Context {
1069 ptr: *mut svn_wc_context_t,
1070 pool: apr::Pool<'static>,
1071 _phantom: PhantomData<*mut ()>, }
1073
1074impl Context {
1075 pub fn close(&mut self) {
1080 if !self.ptr.is_null() {
1081 unsafe {
1082 subversion_sys::svn_wc_context_destroy(self.ptr);
1083 }
1084 self.ptr = std::ptr::null_mut();
1085 }
1086 }
1087}
1088
1089impl Drop for Context {
1090 fn drop(&mut self) {
1091 self.close();
1092 }
1093}
1094
1095pub mod adm;
1096#[allow(deprecated)]
1097pub use adm::Adm;
1098
1099impl Context {
1100 pub fn pool(&self) -> &apr::Pool<'_> {
1102 &self.pool
1103 }
1104
1105 pub fn as_ptr(&self) -> *const svn_wc_context_t {
1107 self.ptr
1108 }
1109
1110 pub fn as_mut_ptr(&mut self) -> *mut svn_wc_context_t {
1112 self.ptr
1113 }
1114
1115 pub fn new() -> Result<Self, crate::Error<'static>> {
1117 let pool = apr::Pool::new();
1118
1119 unsafe {
1120 let mut ctx = std::ptr::null_mut();
1121 with_tmp_pool(|scratch_pool| {
1122 let err = subversion_sys::svn_wc_context_create(
1123 &mut ctx,
1124 std::ptr::null_mut(),
1125 pool.as_mut_ptr(),
1126 scratch_pool.as_mut_ptr(),
1127 );
1128 svn_result(err)
1129 })?;
1130
1131 Ok(Context {
1132 ptr: ctx,
1133 pool,
1134 _phantom: PhantomData,
1135 })
1136 }
1137 }
1138
1139 pub fn new_with_config(config: *mut std::ffi::c_void) -> Result<Self, crate::Error<'static>> {
1141 let pool = apr::Pool::new();
1142
1143 unsafe {
1144 let mut ctx = std::ptr::null_mut();
1145 with_tmp_pool(|scratch_pool| {
1146 let err = subversion_sys::svn_wc_context_create(
1147 &mut ctx,
1148 config as *mut subversion_sys::svn_config_t,
1149 pool.as_mut_ptr(),
1150 scratch_pool.as_mut_ptr(),
1151 );
1152 svn_result(err)
1153 })?;
1154
1155 Ok(Context {
1156 ptr: ctx,
1157 pool,
1158 _phantom: PhantomData,
1159 })
1160 }
1161 }
1162
1163 pub fn check_wc(&mut self, path: &str) -> Result<i32, crate::Error<'_>> {
1165 let scratch_pool = apr::pool::Pool::new();
1166 let path = crate::dirent::to_absolute_cstring(path)?;
1167 let mut wc_format = 0;
1168 let err = unsafe {
1169 subversion_sys::svn_wc_check_wc2(
1170 &mut wc_format,
1171 self.ptr,
1172 path.as_ptr(),
1173 scratch_pool.as_mut_ptr(),
1174 )
1175 };
1176 Error::from_raw(err)?;
1177 Ok(wc_format)
1178 }
1179
1180 pub fn text_modified(&mut self, path: &str) -> Result<bool, crate::Error<'_>> {
1182 let scratch_pool = apr::pool::Pool::new();
1183 let path = crate::dirent::to_absolute_cstring(path)?;
1184 let mut modified = 0;
1185 let err = unsafe {
1186 subversion_sys::svn_wc_text_modified_p2(
1187 &mut modified,
1188 self.ptr,
1189 path.as_ptr(),
1190 0,
1191 scratch_pool.as_mut_ptr(),
1192 )
1193 };
1194 Error::from_raw(err)?;
1195 Ok(modified != 0)
1196 }
1197
1198 pub fn props_modified(&mut self, path: &str) -> Result<bool, crate::Error<'_>> {
1200 let scratch_pool = apr::pool::Pool::new();
1201 let path = crate::dirent::to_absolute_cstring(path)?;
1202 let mut modified = 0;
1203 let err = unsafe {
1204 subversion_sys::svn_wc_props_modified_p2(
1205 &mut modified,
1206 self.ptr,
1207 path.as_ptr(),
1208 scratch_pool.as_mut_ptr(),
1209 )
1210 };
1211 Error::from_raw(err)?;
1212 Ok(modified != 0)
1213 }
1214
1215 pub fn conflicted(&mut self, path: &str) -> Result<(bool, bool, bool), crate::Error<'_>> {
1217 let scratch_pool = apr::pool::Pool::new();
1218 let path = crate::dirent::to_absolute_cstring(path)?;
1219 let mut text_conflicted = 0;
1220 let mut prop_conflicted = 0;
1221 let mut tree_conflicted = 0;
1222 let err = unsafe {
1223 subversion_sys::svn_wc_conflicted_p3(
1224 &mut text_conflicted,
1225 &mut prop_conflicted,
1226 &mut tree_conflicted,
1227 self.ptr,
1228 path.as_ptr(),
1229 scratch_pool.as_mut_ptr(),
1230 )
1231 };
1232 Error::from_raw(err)?;
1233 Ok((
1234 text_conflicted != 0,
1235 prop_conflicted != 0,
1236 tree_conflicted != 0,
1237 ))
1238 }
1239
1240 pub fn ensure_adm(
1242 &mut self,
1243 local_abspath: &str,
1244 url: &str,
1245 repos_root_url: &str,
1246 repos_uuid: &str,
1247 revision: crate::Revnum,
1248 depth: crate::Depth,
1249 ) -> Result<(), crate::Error<'_>> {
1250 let scratch_pool = apr::pool::Pool::new();
1251 let local_abspath = crate::dirent::to_absolute_cstring(local_abspath)?;
1252 let url = crate::uri::canonicalize_uri(url)?;
1253 let url = std::ffi::CString::new(url.as_str()).unwrap();
1254 let repos_root_url = crate::uri::canonicalize_uri(repos_root_url)?;
1255 let repos_root_url = std::ffi::CString::new(repos_root_url.as_str()).unwrap();
1256 let repos_uuid = std::ffi::CString::new(repos_uuid).unwrap();
1257 let err = unsafe {
1258 subversion_sys::svn_wc_ensure_adm4(
1259 self.ptr,
1260 local_abspath.as_ptr(),
1261 url.as_ptr(),
1262 repos_root_url.as_ptr(),
1263 repos_uuid.as_ptr(),
1264 revision.0,
1265 depth.into(),
1266 scratch_pool.as_mut_ptr(),
1267 )
1268 };
1269 Error::from_raw(err)?;
1270 Ok(())
1271 }
1272
1273 pub fn locked(&mut self, path: &str) -> Result<(bool, bool), crate::Error<'_>> {
1276 let path = crate::dirent::to_absolute_cstring(path)?;
1277 let mut locked = 0;
1278 let mut locked_here = 0;
1279 let scratch_pool = apr::pool::Pool::new();
1280 let err = unsafe {
1281 subversion_sys::svn_wc_locked2(
1282 &mut locked_here,
1283 &mut locked,
1284 self.ptr,
1285 path.as_ptr(),
1286 scratch_pool.as_mut_ptr(),
1287 )
1288 };
1289 Error::from_raw(err)?;
1290 Ok((locked != 0, locked_here != 0))
1291 }
1292
1293 pub fn db_version(&self) -> Result<i32, crate::Error<'_>> {
1295 Ok(0) }
1299
1300 pub fn upgrade(&mut self, local_abspath: &str) -> Result<(), crate::Error<'_>> {
1302 let local_abspath_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
1303 let scratch_pool = apr::pool::Pool::new();
1304
1305 let err = unsafe {
1306 subversion_sys::svn_wc_upgrade(
1307 self.ptr,
1308 local_abspath_cstr.as_ptr(),
1309 None, std::ptr::null_mut(), None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
1316 )
1317 };
1318 Error::from_raw(err)?;
1319 Ok(())
1320 }
1321
1322 pub fn relocate(
1324 &mut self,
1325 wcroot_abspath: &str,
1326 from: &str,
1327 to: &str,
1328 ) -> Result<(), crate::Error<'_>> {
1329 let wcroot_abspath_cstr = crate::dirent::to_absolute_cstring(wcroot_abspath)?;
1330 let from_cstr = std::ffi::CString::new(from)?;
1331 let to_cstr = std::ffi::CString::new(to)?;
1332 let scratch_pool = apr::pool::Pool::new();
1333
1334 unsafe extern "C" fn default_validator(
1336 _baton: *mut std::ffi::c_void,
1337 _uuid: *const std::ffi::c_char,
1338 _url: *const std::ffi::c_char,
1339 _root_url: *const std::ffi::c_char,
1340 _pool: *mut apr_sys::apr_pool_t,
1341 ) -> *mut subversion_sys::svn_error_t {
1342 std::ptr::null_mut() }
1344
1345 let err = unsafe {
1346 subversion_sys::svn_wc_relocate4(
1347 self.ptr,
1348 wcroot_abspath_cstr.as_ptr(),
1349 from_cstr.as_ptr(),
1350 to_cstr.as_ptr(),
1351 Some(default_validator),
1352 std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
1354 )
1355 };
1356 Error::from_raw(err)?;
1357 Ok(())
1358 }
1359
1360 pub fn add(
1362 &mut self,
1363 local_abspath: &str,
1364 depth: crate::Depth,
1365 copyfrom_url: Option<&str>,
1366 copyfrom_rev: Option<crate::Revnum>,
1367 ) -> Result<(), crate::Error<'_>> {
1368 let local_abspath_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
1369 let copyfrom_url_cstr = copyfrom_url.map(std::ffi::CString::new).transpose()?;
1370 let scratch_pool = apr::pool::Pool::new();
1371
1372 let err = unsafe {
1373 subversion_sys::svn_wc_add4(
1374 self.ptr,
1375 local_abspath_cstr.as_ptr(),
1376 depth.into(),
1377 copyfrom_url_cstr
1378 .as_ref()
1379 .map_or(std::ptr::null(), |c| c.as_ptr()),
1380 copyfrom_rev.map_or(-1, |r| r.into()),
1381 None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
1386 )
1387 };
1388 Error::from_raw(err)?;
1389 Ok(())
1390 }
1391}
1392
1393pub fn set_adm_dir(name: &str) -> Result<(), crate::Error<'_>> {
1395 let scratch_pool = apr::pool::Pool::new();
1396 let name = std::ffi::CString::new(name).unwrap();
1397 let err =
1398 unsafe { subversion_sys::svn_wc_set_adm_dir(name.as_ptr(), scratch_pool.as_mut_ptr()) };
1399 Error::from_raw(err)?;
1400 Ok(())
1401}
1402
1403pub fn get_adm_dir() -> String {
1405 let pool = apr::pool::Pool::new();
1406 let name = unsafe { subversion_sys::svn_wc_get_adm_dir(pool.as_mut_ptr()) };
1407 unsafe { std::ffi::CStr::from_ptr(name) }
1408 .to_string_lossy()
1409 .into_owned()
1410}
1411
1412pub fn text_modified(
1414 path: &std::path::Path,
1415 force_comparison: bool,
1416) -> Result<bool, crate::Error<'_>> {
1417 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
1418 let mut modified = 0;
1419
1420 with_tmp_pool(|pool| -> Result<(), crate::Error> {
1421 let mut ctx = std::ptr::null_mut();
1422 with_tmp_pool(|scratch_pool| {
1423 let err = unsafe {
1424 subversion_sys::svn_wc_context_create(
1425 &mut ctx,
1426 std::ptr::null_mut(),
1427 pool.as_mut_ptr(),
1428 scratch_pool.as_mut_ptr(),
1429 )
1430 };
1431 svn_result(err)
1432 })?;
1433
1434 let err = unsafe {
1435 subversion_sys::svn_wc_text_modified_p2(
1436 &mut modified,
1437 ctx,
1438 path_cstr.as_ptr(),
1439 if force_comparison { 1 } else { 0 },
1440 pool.as_mut_ptr(),
1441 )
1442 };
1443 Error::from_raw(err)?;
1444 Ok(())
1445 })?;
1446
1447 Ok(modified != 0)
1448}
1449
1450pub fn props_modified(path: &std::path::Path) -> Result<bool, crate::Error<'_>> {
1452 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
1453 let mut modified = 0;
1454
1455 with_tmp_pool(|pool| -> Result<(), crate::Error> {
1456 let mut ctx = std::ptr::null_mut();
1457 with_tmp_pool(|scratch_pool| {
1458 let err = unsafe {
1459 subversion_sys::svn_wc_context_create(
1460 &mut ctx,
1461 std::ptr::null_mut(),
1462 pool.as_mut_ptr(),
1463 scratch_pool.as_mut_ptr(),
1464 )
1465 };
1466 svn_result(err)
1467 })?;
1468
1469 let err = unsafe {
1470 subversion_sys::svn_wc_props_modified_p2(
1471 &mut modified,
1472 ctx,
1473 path_cstr.as_ptr(),
1474 pool.as_mut_ptr(),
1475 )
1476 };
1477 Error::from_raw(err)?;
1478 Ok(())
1479 })?;
1480
1481 Ok(modified != 0)
1482}
1483
1484pub fn is_adm_dir(name: &str) -> bool {
1486 let name_cstr = std::ffi::CString::new(name).unwrap();
1487 let pool = apr::Pool::new();
1488 let result =
1489 unsafe { subversion_sys::svn_wc_is_adm_dir(name_cstr.as_ptr(), pool.as_mut_ptr()) };
1490 result != 0
1491}
1492
1493#[cfg(feature = "ra")]
1495pub fn crawl_revisions5(
1496 wc_ctx: &mut Context,
1497 local_abspath: &str,
1498 reporter: &mut crate::ra::WrapReporter,
1499 restore_files: bool,
1500 depth: crate::Depth,
1501 honor_depth_exclude: bool,
1502 depth_compatibility_trick: bool,
1503 use_commit_times: bool,
1504 notify_func: Option<&dyn Fn(&Notify)>,
1505) -> Result<(), crate::Error<'static>> {
1506 let local_abspath_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
1507
1508 let notify_baton = notify_func
1509 .map(|f| box_notify_baton_borrowed(f))
1510 .unwrap_or(std::ptr::null_mut());
1511
1512 let result = with_tmp_pool(|scratch_pool| {
1513 let err = unsafe {
1514 subversion_sys::svn_wc_crawl_revisions5(
1515 wc_ctx.as_mut_ptr(),
1516 local_abspath_cstr.as_ptr(),
1517 reporter.as_ptr(),
1518 reporter.as_baton(),
1519 if restore_files { 1 } else { 0 },
1520 depth.into(),
1521 if honor_depth_exclude { 1 } else { 0 },
1522 if depth_compatibility_trick { 1 } else { 0 },
1523 if use_commit_times { 1 } else { 0 },
1524 None, std::ptr::null_mut(), if notify_func.is_some() {
1527 Some(wrap_notify_func)
1528 } else {
1529 None
1530 },
1531 notify_baton,
1532 scratch_pool.as_mut_ptr(),
1533 )
1534 };
1535
1536 svn_result(err)
1537 });
1538
1539 if !notify_baton.is_null() {
1540 unsafe { drop_notify_baton_borrowed(notify_baton) };
1541 }
1542
1543 result
1544}
1545
1546pub fn get_update_editor4<'a>(
1550 wc_ctx: &'a mut Context,
1551 anchor_abspath: &str,
1552 target_basename: &str,
1553 options: UpdateEditorOptions,
1554) -> Result<(UpdateEditor<'a>, crate::Revnum), crate::Error<'static>> {
1555 let anchor_abspath_cstr = crate::dirent::to_absolute_cstring(anchor_abspath)?;
1556 let target_basename_cstr = std::ffi::CString::new(target_basename)?;
1557 let diff3_cmd_cstr = options.diff3_cmd.map(std::ffi::CString::new).transpose()?;
1558
1559 let result_pool = apr::Pool::new();
1560
1561 let preserved_exts_cstrs: Vec<std::ffi::CString> = options
1563 .preserved_exts
1564 .iter()
1565 .map(|&s| std::ffi::CString::new(s))
1566 .collect::<Result<Vec<_>, _>>()?;
1567 let preserved_exts_apr = if preserved_exts_cstrs.is_empty() {
1568 std::ptr::null()
1569 } else {
1570 let mut arr = apr::tables::TypedArray::<*const i8>::new(
1571 &result_pool,
1572 preserved_exts_cstrs.len() as i32,
1573 );
1574 for cstr in &preserved_exts_cstrs {
1575 arr.push(cstr.as_ptr());
1576 }
1577 unsafe { arr.as_ptr() }
1578 };
1579 let mut target_revision: subversion_sys::svn_revnum_t = 0;
1580 let mut editor_ptr: *const subversion_sys::svn_delta_editor_t = std::ptr::null();
1581 let mut edit_baton: *mut std::ffi::c_void = std::ptr::null_mut();
1582
1583 let has_fetch_dirents = options.fetch_dirents_func.is_some();
1585 let fetch_dirents_baton = options
1586 .fetch_dirents_func
1587 .map(|f| box_fetch_dirents_baton(f))
1588 .unwrap_or(std::ptr::null_mut());
1589 let has_conflict = options.conflict_func.is_some();
1590 let conflict_baton = options
1591 .conflict_func
1592 .map(|f| box_conflict_baton(f))
1593 .unwrap_or(std::ptr::null_mut());
1594 let has_external = options.external_func.is_some();
1595 let external_baton = options
1596 .external_func
1597 .map(|f| box_external_baton(f))
1598 .unwrap_or(std::ptr::null_mut());
1599 let has_cancel = options.cancel_func.is_some();
1600 let cancel_baton = options
1601 .cancel_func
1602 .map(box_cancel_baton)
1603 .unwrap_or(std::ptr::null_mut());
1604 let has_notify = options.notify_func.is_some();
1605 let notify_baton = options
1606 .notify_func
1607 .map(|f| box_notify_baton(f))
1608 .unwrap_or(std::ptr::null_mut());
1609
1610 let err = with_tmp_pool(|scratch_pool| unsafe {
1611 svn_result(subversion_sys::svn_wc_get_update_editor4(
1612 &mut editor_ptr,
1613 &mut edit_baton,
1614 &mut target_revision,
1615 wc_ctx.as_mut_ptr(),
1616 anchor_abspath_cstr.as_ptr(),
1617 target_basename_cstr.as_ptr(),
1618 if options.use_commit_times { 1 } else { 0 },
1619 options.depth.into(),
1620 if options.depth_is_sticky { 1 } else { 0 },
1621 if options.allow_unver_obstructions {
1622 1
1623 } else {
1624 0
1625 },
1626 if options.adds_as_modification { 1 } else { 0 },
1627 if options.server_performs_filtering {
1628 1
1629 } else {
1630 0
1631 },
1632 if options.clean_checkout { 1 } else { 0 },
1633 diff3_cmd_cstr
1634 .as_ref()
1635 .map_or(std::ptr::null(), |c| c.as_ptr()),
1636 preserved_exts_apr,
1637 if has_fetch_dirents {
1638 Some(wrap_fetch_dirents_func)
1639 } else {
1640 None
1641 },
1642 fetch_dirents_baton,
1643 if has_conflict {
1644 Some(wrap_conflict_func)
1645 } else {
1646 None
1647 },
1648 conflict_baton,
1649 if has_external {
1650 Some(wrap_external_func)
1651 } else {
1652 None
1653 },
1654 external_baton,
1655 if has_cancel {
1656 Some(crate::wrap_cancel_func)
1657 } else {
1658 None
1659 },
1660 cancel_baton,
1661 if has_notify {
1662 Some(wrap_notify_func)
1663 } else {
1664 None
1665 },
1666 notify_baton,
1667 result_pool.as_mut_ptr(),
1668 scratch_pool.as_mut_ptr(),
1669 ))
1670 });
1671
1672 err?;
1673
1674 let mut batons = Vec::new();
1677 if !fetch_dirents_baton.is_null() {
1678 batons.push((fetch_dirents_baton, drop_fetch_dirents_baton as DropperFn));
1679 }
1680 if !conflict_baton.is_null() {
1681 batons.push((conflict_baton, drop_conflict_baton as DropperFn));
1682 }
1683 if !external_baton.is_null() {
1684 batons.push((external_baton, drop_external_baton as DropperFn));
1685 }
1686 if !cancel_baton.is_null() {
1687 batons.push((cancel_baton, drop_cancel_baton as DropperFn));
1688 }
1689 if !notify_baton.is_null() {
1690 batons.push((notify_baton, drop_notify_baton as DropperFn));
1691 }
1692
1693 let editor = UpdateEditor {
1694 editor: editor_ptr,
1695 edit_baton,
1696 _pool: result_pool,
1697 target_revision: crate::Revnum::from_raw(target_revision).unwrap_or_default(),
1698 callback_batons: batons,
1699 _marker: std::marker::PhantomData,
1700 };
1701
1702 Ok((
1703 editor,
1704 crate::Revnum::from_raw(target_revision).unwrap_or_default(),
1705 ))
1706}
1707
1708type DropperFn = unsafe fn(*mut std::ffi::c_void);
1710
1711#[derive(Default)]
1713pub struct UpdateEditorOptions<'a> {
1714 pub use_commit_times: bool,
1716 pub depth: crate::Depth,
1718 pub depth_is_sticky: bool,
1720 pub allow_unver_obstructions: bool,
1722 pub adds_as_modification: bool,
1724 pub server_performs_filtering: bool,
1726 pub clean_checkout: bool,
1728 pub diff3_cmd: Option<&'a str>,
1730 pub preserved_exts: Vec<&'a str>,
1732 pub fetch_dirents_func: Option<
1734 Box<
1735 dyn Fn(
1736 &str,
1737 &str,
1738 )
1739 -> Result<std::collections::HashMap<String, crate::DirEntry>, Error<'static>>,
1740 >,
1741 >,
1742 pub conflict_func: Option<
1744 Box<
1745 dyn Fn(
1746 &crate::conflict::ConflictDescription,
1747 ) -> Result<crate::conflict::ConflictResult, Error<'static>>,
1748 >,
1749 >,
1750 pub external_func: Option<
1752 Box<dyn Fn(&str, Option<&str>, Option<&str>, crate::Depth) -> Result<(), Error<'static>>>,
1753 >,
1754 pub cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
1756 pub notify_func: Option<Box<dyn Fn(&Notify)>>,
1758}
1759
1760impl<'a> UpdateEditorOptions<'a> {
1761 pub fn new() -> Self {
1763 Self::default()
1764 }
1765
1766 pub fn with_use_commit_times(mut self, use_commit_times: bool) -> Self {
1768 self.use_commit_times = use_commit_times;
1769 self
1770 }
1771
1772 pub fn with_depth(mut self, depth: crate::Depth) -> Self {
1774 self.depth = depth;
1775 self
1776 }
1777
1778 pub fn with_depth_is_sticky(mut self, sticky: bool) -> Self {
1780 self.depth_is_sticky = sticky;
1781 self
1782 }
1783
1784 pub fn with_allow_unver_obstructions(mut self, allow: bool) -> Self {
1786 self.allow_unver_obstructions = allow;
1787 self
1788 }
1789
1790 pub fn with_adds_as_modification(mut self, adds_as_mod: bool) -> Self {
1792 self.adds_as_modification = adds_as_mod;
1793 self
1794 }
1795
1796 pub fn with_diff3_cmd(mut self, cmd: &'a str) -> Self {
1798 self.diff3_cmd = Some(cmd);
1799 self
1800 }
1801
1802 pub fn with_preserved_exts(mut self, exts: Vec<&'a str>) -> Self {
1804 self.preserved_exts = exts;
1805 self
1806 }
1807}
1808
1809#[derive(Default)]
1811pub struct SwitchEditorOptions<'a> {
1812 pub use_commit_times: bool,
1814 pub depth: crate::Depth,
1816 pub depth_is_sticky: bool,
1818 pub allow_unver_obstructions: bool,
1820 pub server_performs_filtering: bool,
1822 pub diff3_cmd: Option<&'a str>,
1824 pub preserved_exts: Vec<&'a str>,
1826 pub fetch_dirents_func: Option<
1828 Box<
1829 dyn Fn(
1830 &str,
1831 &str,
1832 )
1833 -> Result<std::collections::HashMap<String, crate::DirEntry>, Error<'static>>,
1834 >,
1835 >,
1836 pub conflict_func: Option<
1838 Box<
1839 dyn Fn(
1840 &crate::conflict::ConflictDescription,
1841 ) -> Result<crate::conflict::ConflictResult, Error<'static>>,
1842 >,
1843 >,
1844 pub external_func: Option<
1846 Box<dyn Fn(&str, Option<&str>, Option<&str>, crate::Depth) -> Result<(), Error<'static>>>,
1847 >,
1848 pub cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
1850 pub notify_func: Option<Box<dyn Fn(&Notify)>>,
1852}
1853
1854impl<'a> SwitchEditorOptions<'a> {
1855 pub fn new() -> Self {
1857 Self::default()
1858 }
1859
1860 pub fn with_use_commit_times(mut self, use_commit_times: bool) -> Self {
1862 self.use_commit_times = use_commit_times;
1863 self
1864 }
1865
1866 pub fn with_depth(mut self, depth: crate::Depth) -> Self {
1868 self.depth = depth;
1869 self
1870 }
1871
1872 pub fn with_depth_is_sticky(mut self, sticky: bool) -> Self {
1874 self.depth_is_sticky = sticky;
1875 self
1876 }
1877
1878 pub fn with_allow_unver_obstructions(mut self, allow: bool) -> Self {
1880 self.allow_unver_obstructions = allow;
1881 self
1882 }
1883
1884 pub fn with_diff3_cmd(mut self, cmd: &'a str) -> Self {
1886 self.diff3_cmd = Some(cmd);
1887 self
1888 }
1889
1890 pub fn with_preserved_exts(mut self, exts: Vec<&'a str>) -> Self {
1892 self.preserved_exts = exts;
1893 self
1894 }
1895}
1896
1897pub struct UpdateEditor<'a> {
1901 editor: *const subversion_sys::svn_delta_editor_t,
1902 edit_baton: *mut std::ffi::c_void,
1903 _pool: apr::Pool<'static>,
1904 target_revision: crate::Revnum,
1905 callback_batons: Vec<(*mut std::ffi::c_void, DropperFn)>,
1907 _marker: std::marker::PhantomData<&'a Context>,
1908}
1909
1910impl Drop for UpdateEditor<'_> {
1911 fn drop(&mut self) {
1912 for (baton, dropper) in &self.callback_batons {
1914 if !baton.is_null() {
1915 unsafe {
1916 dropper(*baton);
1917 }
1918 }
1919 }
1920 self.callback_batons.clear();
1921 }
1922}
1923
1924impl UpdateEditor<'_> {
1925 pub fn target_revision(&self) -> crate::Revnum {
1927 self.target_revision
1928 }
1929
1930 pub fn into_wrap_editor(mut self) -> crate::delta::WrapEditor<'static> {
1935 let editor = self.editor;
1936 let baton = self.edit_baton;
1937 let pool = std::mem::replace(&mut self._pool, apr::Pool::new());
1938 let batons = std::mem::take(&mut self.callback_batons);
1939
1940 std::mem::forget(self);
1943
1944 crate::delta::WrapEditor {
1945 editor,
1946 baton,
1947 _pool: apr::pool::PoolHandle::owned(pool),
1948 callback_batons: batons,
1949 }
1950 }
1951}
1952
1953impl crate::delta::Editor for UpdateEditor<'_> {
1954 type RootEditor = crate::delta::WrapDirectoryEditor<'static>;
1955
1956 fn set_target_revision(
1957 &mut self,
1958 revision: Option<crate::Revnum>,
1959 ) -> Result<(), crate::Error<'_>> {
1960 let scratch_pool = apr::Pool::new();
1961 let err = unsafe {
1962 ((*self.editor).set_target_revision.unwrap())(
1963 self.edit_baton,
1964 revision.map_or(-1, |r| r.into()),
1965 scratch_pool.as_mut_ptr(),
1966 )
1967 };
1968 crate::Error::from_raw(err)?;
1969 Ok(())
1970 }
1971
1972 fn open_root(
1973 &mut self,
1974 base_revision: Option<crate::Revnum>,
1975 ) -> Result<crate::delta::WrapDirectoryEditor<'static>, crate::Error<'_>> {
1976 let mut baton = std::ptr::null_mut();
1977 let pool = apr::Pool::new();
1978 let err = unsafe {
1979 ((*self.editor).open_root.unwrap())(
1980 self.edit_baton,
1981 base_revision.map_or(-1, |r| r.into()),
1982 pool.as_mut_ptr(),
1983 &mut baton,
1984 )
1985 };
1986 crate::Error::from_raw(err)?;
1987 Ok(crate::delta::WrapDirectoryEditor {
1988 editor: self.editor,
1989 baton,
1990 _pool: apr::PoolHandle::owned(pool),
1991 })
1992 }
1993
1994 fn close(&mut self) -> Result<(), crate::Error<'_>> {
1995 let scratch_pool = apr::Pool::new();
1996 let err = unsafe {
1997 ((*self.editor).close_edit.unwrap())(self.edit_baton, scratch_pool.as_mut_ptr())
1998 };
1999 crate::Error::from_raw(err)?;
2000 Ok(())
2001 }
2002
2003 fn abort(&mut self) -> Result<(), crate::Error<'_>> {
2004 let scratch_pool = apr::Pool::new();
2005 let err = unsafe {
2006 ((*self.editor).abort_edit.unwrap())(self.edit_baton, scratch_pool.as_mut_ptr())
2007 };
2008 crate::Error::from_raw(err)?;
2009 Ok(())
2010 }
2011}
2012
2013pub type DirEntries = std::collections::HashMap<String, crate::DirEntry>;
2015
2016pub fn check_wc(path: &std::path::Path) -> Result<Option<i32>, crate::Error<'_>> {
2018 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2019 let mut wc_format = 0;
2020
2021 with_tmp_pool(|pool| -> Result<(), crate::Error> {
2022 let mut ctx = std::ptr::null_mut();
2023 with_tmp_pool(|scratch_pool| {
2024 let err = unsafe {
2025 subversion_sys::svn_wc_context_create(
2026 &mut ctx,
2027 std::ptr::null_mut(),
2028 pool.as_mut_ptr(),
2029 scratch_pool.as_mut_ptr(),
2030 )
2031 };
2032 svn_result(err)
2033 })?;
2034
2035 let err = unsafe {
2036 subversion_sys::svn_wc_check_wc2(
2037 &mut wc_format,
2038 ctx,
2039 path_cstr.as_ptr(),
2040 pool.as_mut_ptr(),
2041 )
2042 };
2043 Error::from_raw(err)?;
2044 Ok(())
2045 })?;
2046
2047 if wc_format == 0 {
2049 Ok(None)
2050 } else {
2051 Ok(Some(wc_format))
2052 }
2053}
2054
2055pub fn ensure_adm(
2057 path: &std::path::Path,
2058 uuid: &str,
2059 url: &str,
2060 repos_root: &str,
2061 revision: i64,
2062) -> Result<(), crate::Error<'static>> {
2063 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2064 let uuid_cstr = std::ffi::CString::new(uuid).unwrap();
2065 let url = crate::uri::canonicalize_uri(url)?;
2066 let url_cstr = std::ffi::CString::new(url.as_str()).unwrap();
2067 let repos_root = crate::uri::canonicalize_uri(repos_root)?;
2068 let repos_root_cstr = std::ffi::CString::new(repos_root.as_str()).unwrap();
2069
2070 with_tmp_pool(|pool| -> Result<(), crate::Error> {
2071 let mut ctx = std::ptr::null_mut();
2072 with_tmp_pool(|scratch_pool| {
2073 let err = unsafe {
2074 subversion_sys::svn_wc_context_create(
2075 &mut ctx,
2076 std::ptr::null_mut(),
2077 pool.as_mut_ptr(),
2078 scratch_pool.as_mut_ptr(),
2079 )
2080 };
2081 svn_result(err)
2082 })?;
2083
2084 let err = unsafe {
2085 subversion_sys::svn_wc_ensure_adm4(
2086 ctx,
2087 path_cstr.as_ptr(),
2088 url_cstr.as_ptr(),
2089 repos_root_cstr.as_ptr(),
2090 uuid_cstr.as_ptr(),
2091 revision as subversion_sys::svn_revnum_t,
2092 subversion_sys::svn_depth_t_svn_depth_infinity,
2093 pool.as_mut_ptr(),
2094 )
2095 };
2096 Error::from_raw(err)?;
2097 Ok(())
2098 })
2099}
2100
2101pub fn is_normal_prop(name: &str) -> bool {
2103 let name_cstr = std::ffi::CString::new(name).unwrap();
2104 unsafe { subversion_sys::svn_wc_is_normal_prop(name_cstr.as_ptr()) != 0 }
2105}
2106
2107pub fn is_entry_prop(name: &str) -> bool {
2109 let name_cstr = std::ffi::CString::new(name).unwrap();
2110 unsafe { subversion_sys::svn_wc_is_entry_prop(name_cstr.as_ptr()) != 0 }
2111}
2112
2113pub fn is_wc_prop(name: &str) -> bool {
2115 let name_cstr = std::ffi::CString::new(name).unwrap();
2116 unsafe { subversion_sys::svn_wc_is_wc_prop(name_cstr.as_ptr()) != 0 }
2117}
2118
2119pub fn match_ignore_list(path: &str, patterns: &[&str]) -> Result<bool, crate::Error<'static>> {
2121 let path_cstr = std::ffi::CString::new(path)?;
2122
2123 with_tmp_pool(|pool| {
2124 let pattern_cstrs: Vec<std::ffi::CString> = patterns
2126 .iter()
2127 .map(|p| std::ffi::CString::new(*p))
2128 .collect::<Result<Vec<_>, _>>()?;
2129
2130 let mut patterns_array =
2132 apr::tables::TypedArray::<*const i8>::new(pool, patterns.len() as i32);
2133 for pattern_cstr in &pattern_cstrs {
2134 patterns_array.push(pattern_cstr.as_ptr());
2135 }
2136
2137 let matched = unsafe {
2138 subversion_sys::svn_wc_match_ignore_list(
2139 path_cstr.as_ptr(),
2140 patterns_array.as_ptr(),
2141 pool.as_mut_ptr(),
2142 )
2143 };
2144
2145 Ok(matched != 0)
2147 })
2148}
2149
2150pub fn get_actual_target(path: &std::path::Path) -> Result<(String, String), crate::Error<'_>> {
2152 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2153 let mut anchor: *const i8 = std::ptr::null();
2154 let mut target: *const i8 = std::ptr::null();
2155
2156 with_tmp_pool(|pool| -> Result<(), crate::Error> {
2157 let mut ctx = std::ptr::null_mut();
2158 with_tmp_pool(|scratch_pool| {
2159 let err = unsafe {
2160 subversion_sys::svn_wc_context_create(
2161 &mut ctx,
2162 std::ptr::null_mut(),
2163 pool.as_mut_ptr(),
2164 scratch_pool.as_mut_ptr(),
2165 )
2166 };
2167 svn_result(err)
2168 })?;
2169
2170 let err = unsafe {
2171 subversion_sys::svn_wc_get_actual_target2(
2172 &mut anchor,
2173 &mut target,
2174 ctx,
2175 path_cstr.as_ptr(),
2176 pool.as_mut_ptr(),
2177 pool.as_mut_ptr(),
2178 )
2179 };
2180 Error::from_raw(err)?;
2181 Ok(())
2182 })?;
2183
2184 let anchor_str = if anchor.is_null() {
2185 String::new()
2186 } else {
2187 unsafe { std::ffi::CStr::from_ptr(anchor) }
2188 .to_string_lossy()
2189 .into_owned()
2190 };
2191
2192 let target_str = if target.is_null() {
2193 String::new()
2194 } else {
2195 unsafe { std::ffi::CStr::from_ptr(target) }
2196 .to_string_lossy()
2197 .into_owned()
2198 };
2199
2200 Ok((anchor_str, target_str))
2201}
2202
2203pub fn get_pristine_contents(
2205 path: &std::path::Path,
2206) -> Result<Option<crate::io::Stream>, crate::Error<'_>> {
2207 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2208 let mut contents: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();
2209
2210 let result_pool = apr::Pool::new();
2212
2213 with_tmp_pool(|scratch_pool| -> Result<(), crate::Error> {
2214 let mut ctx = std::ptr::null_mut();
2215 with_tmp_pool(|ctx_scratch_pool| {
2216 let err = unsafe {
2217 subversion_sys::svn_wc_context_create(
2218 &mut ctx,
2219 std::ptr::null_mut(),
2220 scratch_pool.as_mut_ptr(), ctx_scratch_pool.as_mut_ptr(),
2222 )
2223 };
2224 svn_result(err)
2225 })?;
2226
2227 let err = unsafe {
2228 subversion_sys::svn_wc_get_pristine_contents2(
2229 &mut contents,
2230 ctx,
2231 path_cstr.as_ptr(),
2232 result_pool.as_mut_ptr(), scratch_pool.as_mut_ptr(), )
2235 };
2236 Error::from_raw(err)?;
2237 Ok(())
2238 })?;
2239
2240 if contents.is_null() {
2241 Ok(None)
2242 } else {
2243 Ok(Some(unsafe {
2244 crate::io::Stream::from_ptr_and_pool(contents, result_pool)
2245 }))
2246 }
2247}
2248
2249pub fn get_pristine_copy_path(
2251 path: &std::path::Path,
2252) -> Result<std::path::PathBuf, crate::Error<'_>> {
2253 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2254 let mut pristine_path: *const i8 = std::ptr::null();
2255
2256 let pristine_path_str = with_tmp_pool(|pool| -> Result<String, crate::Error> {
2257 let err = unsafe {
2258 subversion_sys::svn_wc_get_pristine_copy_path(
2259 path_cstr.as_ptr(),
2260 &mut pristine_path,
2261 pool.as_mut_ptr(),
2262 )
2263 };
2264 Error::from_raw(err)?;
2265
2266 let result = if pristine_path.is_null() {
2268 String::new()
2269 } else {
2270 unsafe { std::ffi::CStr::from_ptr(pristine_path) }
2271 .to_string_lossy()
2272 .into_owned()
2273 };
2274 Ok(result)
2275 })?;
2276
2277 Ok(std::path::PathBuf::from(pristine_path_str))
2278}
2279
2280impl Context {
2281 pub fn get_actual_target(&mut self, path: &str) -> Result<(String, String), crate::Error<'_>> {
2283 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2284 let mut anchor: *const i8 = std::ptr::null();
2285 let mut target: *const i8 = std::ptr::null();
2286
2287 let pool = apr::Pool::new();
2288 let err = unsafe {
2289 subversion_sys::svn_wc_get_actual_target2(
2290 &mut anchor,
2291 &mut target,
2292 self.ptr,
2293 path_cstr.as_ptr(),
2294 pool.as_mut_ptr(),
2295 pool.as_mut_ptr(),
2296 )
2297 };
2298 Error::from_raw(err)?;
2299
2300 let anchor_str = if anchor.is_null() {
2301 String::new()
2302 } else {
2303 unsafe { std::ffi::CStr::from_ptr(anchor) }
2304 .to_string_lossy()
2305 .into_owned()
2306 };
2307
2308 let target_str = if target.is_null() {
2309 String::new()
2310 } else {
2311 unsafe { std::ffi::CStr::from_ptr(target) }
2312 .to_string_lossy()
2313 .into_owned()
2314 };
2315
2316 Ok((anchor_str, target_str))
2317 }
2318
2319 pub fn get_pristine_contents(
2321 &mut self,
2322 path: &str,
2323 ) -> Result<Option<crate::io::Stream>, crate::Error<'_>> {
2324 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2325 let mut contents: *mut subversion_sys::svn_stream_t = std::ptr::null_mut();
2326
2327 let pool = apr::Pool::new();
2328 let err = unsafe {
2329 subversion_sys::svn_wc_get_pristine_contents2(
2330 &mut contents,
2331 self.ptr,
2332 path_cstr.as_ptr(),
2333 pool.as_mut_ptr(),
2334 pool.as_mut_ptr(),
2335 )
2336 };
2337 Error::from_raw(err)?;
2338
2339 if contents.is_null() {
2340 Ok(None)
2341 } else {
2342 Ok(Some(unsafe {
2343 crate::io::Stream::from_ptr_and_pool(contents, pool)
2344 }))
2345 }
2346 }
2347
2348 pub fn get_pristine_props(
2350 &mut self,
2351 path: &str,
2352 ) -> Result<Option<std::collections::HashMap<String, Vec<u8>>>, crate::Error<'_>> {
2353 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2354 let mut props: *mut apr_sys::apr_hash_t = std::ptr::null_mut();
2355
2356 let pool = apr::Pool::new();
2357 let err = unsafe {
2358 subversion_sys::svn_wc_get_pristine_props(
2359 &mut props,
2360 self.ptr,
2361 path_cstr.as_ptr(),
2362 pool.as_mut_ptr(),
2363 pool.as_mut_ptr(),
2364 )
2365 };
2366 Error::from_raw(err)?;
2367
2368 if props.is_null() {
2369 return Ok(None);
2370 }
2371
2372 let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
2373 Ok(Some(prop_hash.to_hashmap()))
2374 }
2375
2376 pub fn walk_status<F>(
2381 &mut self,
2382 local_abspath: &std::path::Path,
2383 depth: crate::Depth,
2384 get_all: bool,
2385 no_ignore: bool,
2386 ignore_text_mods: bool,
2387 ignore_patterns: Option<&[&str]>,
2388 status_func: F,
2389 ) -> Result<(), Error<'static>>
2390 where
2391 F: FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>,
2392 {
2393 let pool = apr::Pool::new();
2394 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
2395
2396 let pattern_cstrs: Vec<std::ffi::CString> = ignore_patterns
2398 .unwrap_or(&[])
2399 .iter()
2400 .map(|p| std::ffi::CString::new(*p).expect("pattern valid UTF-8"))
2401 .collect();
2402 let ignore_patterns_ptr = if let Some(patterns) = ignore_patterns {
2403 let mut arr = apr::tables::TypedArray::<*const std::os::raw::c_char>::new(
2404 &pool,
2405 patterns.len() as i32,
2406 );
2407 for cstr in &pattern_cstrs {
2408 arr.push(cstr.as_ptr());
2409 }
2410 unsafe { arr.as_ptr() }
2411 } else {
2412 std::ptr::null_mut()
2413 };
2414
2415 unsafe extern "C" fn status_callback(
2417 baton: *mut std::ffi::c_void,
2418 local_abspath: *const std::os::raw::c_char,
2419 status: *const subversion_sys::svn_wc_status3_t,
2420 scratch_pool: *mut apr_sys::apr_pool_t,
2421 ) -> *mut subversion_sys::svn_error_t {
2422 let callback = unsafe {
2423 &mut *(baton
2424 as *mut Box<dyn FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>>)
2425 };
2426
2427 let local_path =
2428 unsafe { subversion_sys::svn_dirent_local_style(local_abspath, scratch_pool) };
2429 let path = unsafe {
2430 std::ffi::CStr::from_ptr(local_path)
2431 .to_string_lossy()
2432 .into_owned()
2433 };
2434
2435 let status = Status {
2439 ptr: status,
2440 _pool: unsafe { apr::pool::PoolHandle::from_borrowed_raw(scratch_pool) },
2441 };
2442
2443 match callback(&path, &status) {
2444 Ok(()) => std::ptr::null_mut(),
2445 Err(e) => unsafe { e.into_raw() },
2446 }
2447 }
2448
2449 let boxed_callback: Box<Box<dyn FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>>> =
2450 Box::new(Box::new(status_func));
2451 let baton = Box::into_raw(boxed_callback) as *mut std::ffi::c_void;
2452
2453 unsafe {
2454 let err = subversion_sys::svn_wc_walk_status(
2455 self.ptr,
2456 path_cstr.as_ptr(),
2457 depth.into(),
2458 get_all as i32,
2459 no_ignore as i32,
2460 ignore_text_mods as i32,
2461 ignore_patterns_ptr,
2462 Some(status_callback),
2463 baton,
2464 None, std::ptr::null_mut(), pool.as_mut_ptr(),
2467 );
2468
2469 let _ = Box::from_raw(
2471 baton as *mut Box<dyn FnMut(&str, &Status<'_>) -> Result<(), Error<'static>>>,
2472 );
2473
2474 Error::from_raw(err)
2475 }
2476 }
2477
2478 pub fn queue_committed(
2487 &mut self,
2488 local_abspath: &std::path::Path,
2489 recurse: bool,
2490 is_committed: bool,
2491 committed_queue: &mut CommittedQueue,
2492 wcprop_changes: Option<&[PropChange]>,
2493 remove_lock: bool,
2494 remove_changelist: bool,
2495 sha1_checksum: Option<&crate::Checksum>,
2496 ) -> Result<(), Error<'static>> {
2497 let pool = apr::Pool::new();
2498 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
2499
2500 let wcprop_changes_ptr = if let Some(changes) = wcprop_changes {
2502 let prop_name_cstrs: Vec<std::ffi::CString> = changes
2503 .iter()
2504 .map(|c| std::ffi::CString::new(c.name.as_str()).expect("prop name valid UTF-8"))
2505 .collect();
2506 let mut arr = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(
2507 &pool,
2508 changes.len() as i32,
2509 );
2510 for (change, name_cstr) in changes.iter().zip(prop_name_cstrs.iter()) {
2511 arr.push(subversion_sys::svn_prop_t {
2512 name: name_cstr.as_ptr(),
2513 value: if let Some(v) = &change.value {
2514 crate::svn_string_helpers::svn_string_ncreate(v, &pool)
2515 } else {
2516 std::ptr::null()
2517 },
2518 });
2519 }
2520 unsafe { arr.as_ptr() }
2521 } else {
2522 std::ptr::null()
2523 };
2524
2525 let sha1_ptr = sha1_checksum.map(|c| c.ptr).unwrap_or(std::ptr::null());
2526
2527 unsafe {
2528 let err = subversion_sys::svn_wc_queue_committed4(
2529 committed_queue.as_mut_ptr(),
2530 self.ptr,
2531 path_cstr.as_ptr(),
2532 recurse as i32,
2533 is_committed as i32,
2534 wcprop_changes_ptr,
2535 remove_lock as i32,
2536 remove_changelist as i32,
2537 sha1_ptr,
2538 pool.as_mut_ptr(),
2539 );
2540 Error::from_raw(err)
2541 }
2542 }
2543
2544 pub fn process_committed_queue(
2548 &mut self,
2549 committed_queue: &mut CommittedQueue,
2550 new_revnum: crate::Revnum,
2551 rev_date: Option<&str>,
2552 rev_author: Option<&str>,
2553 ) -> Result<(), Error<'static>> {
2554 let pool = apr::Pool::new();
2555
2556 let rev_date_cstr = rev_date.map(std::ffi::CString::new).transpose()?;
2557 let rev_author_cstr = rev_author.map(std::ffi::CString::new).transpose()?;
2558
2559 unsafe {
2560 let err = subversion_sys::svn_wc_process_committed_queue2(
2561 committed_queue.as_mut_ptr(),
2562 self.ptr,
2563 new_revnum.0,
2564 rev_date_cstr
2565 .as_ref()
2566 .map_or(std::ptr::null(), |s| s.as_ptr()),
2567 rev_author_cstr
2568 .as_ref()
2569 .map_or(std::ptr::null(), |s| s.as_ptr()),
2570 None, std::ptr::null_mut(), pool.as_mut_ptr(),
2573 );
2574 Error::from_raw(err)
2575 }
2576 }
2577
2578 pub fn add_lock(
2582 &mut self,
2583 local_abspath: &std::path::Path,
2584 lock: &Lock,
2585 ) -> Result<(), Error<'static>> {
2586 let pool = apr::Pool::new();
2587 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
2588
2589 unsafe {
2590 let err = subversion_sys::svn_wc_add_lock2(
2591 self.ptr,
2592 path_cstr.as_ptr(),
2593 lock.as_ptr(),
2594 pool.as_mut_ptr(),
2595 );
2596 Error::from_raw(err)
2597 }
2598 }
2599
2600 pub fn remove_lock(&mut self, local_abspath: &std::path::Path) -> Result<(), Error<'static>> {
2604 let pool = apr::Pool::new();
2605 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
2606
2607 unsafe {
2608 let err = subversion_sys::svn_wc_remove_lock2(
2609 self.ptr,
2610 path_cstr.as_ptr(),
2611 pool.as_mut_ptr(),
2612 );
2613 Error::from_raw(err)
2614 }
2615 }
2616
2617 pub fn crop_tree(
2622 &mut self,
2623 local_abspath: &std::path::Path,
2624 depth: crate::Depth,
2625 cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
2626 ) -> Result<(), Error<'static>> {
2627 let pool = apr::Pool::new();
2628 let path = local_abspath.to_str().unwrap();
2629 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2630
2631 let cancel_baton = cancel_func
2632 .map(box_cancel_baton_borrowed)
2633 .unwrap_or(std::ptr::null_mut());
2634
2635 let ret = unsafe {
2636 subversion_sys::svn_wc_crop_tree2(
2637 self.ptr,
2638 path_cstr.as_ptr(),
2639 depth.into(),
2640 if cancel_func.is_some() {
2641 Some(crate::wrap_cancel_func)
2642 } else {
2643 None
2644 },
2645 cancel_baton,
2646 None, std::ptr::null_mut(), pool.as_mut_ptr(),
2649 )
2650 };
2651
2652 if !cancel_baton.is_null() {
2654 unsafe { drop_cancel_baton_borrowed(cancel_baton) };
2655 }
2656
2657 Error::from_raw(ret)
2658 }
2659
2660 pub fn resolved_conflict(
2665 &mut self,
2666 local_abspath: &std::path::Path,
2667 depth: crate::Depth,
2668 resolve_text: bool,
2669 resolve_property: Option<&str>,
2670 resolve_tree: bool,
2671 conflict_choice: ConflictChoice,
2672 cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
2673 ) -> Result<(), Error<'static>> {
2674 let pool = apr::Pool::new();
2675 let path = local_abspath.to_str().unwrap();
2676 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2677
2678 let prop_cstr = resolve_property.map(|p| std::ffi::CString::new(p).unwrap());
2679 let prop_ptr = prop_cstr.as_ref().map_or(std::ptr::null(), |p| p.as_ptr());
2680
2681 let cancel_baton = cancel_func
2682 .map(box_cancel_baton_borrowed)
2683 .unwrap_or(std::ptr::null_mut());
2684
2685 let ret = unsafe {
2686 subversion_sys::svn_wc_resolved_conflict5(
2687 self.ptr,
2688 path_cstr.as_ptr(),
2689 depth.into(),
2690 resolve_text.into(),
2691 prop_ptr,
2692 resolve_tree.into(),
2693 conflict_choice.into(),
2694 if cancel_func.is_some() {
2695 Some(crate::wrap_cancel_func)
2696 } else {
2697 None
2698 },
2699 cancel_baton,
2700 None, std::ptr::null_mut(), pool.as_mut_ptr(),
2703 )
2704 };
2705
2706 if !cancel_baton.is_null() {
2708 unsafe {
2709 drop(Box::from_raw(
2710 cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
2711 ))
2712 };
2713 }
2714
2715 Error::from_raw(ret)
2716 }
2717
2718 pub fn add_from_disk(
2722 &mut self,
2723 local_abspath: &std::path::Path,
2724 props: Option<&std::collections::HashMap<String, Vec<u8>>>,
2725 skip_checks: bool,
2726 notify_func: Option<&dyn Fn(&Notify)>,
2727 ) -> Result<(), Error<'static>> {
2728 let path = local_abspath.to_str().unwrap();
2729 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
2730 let pool = apr::Pool::new();
2731
2732 let props_hash = if let Some(props) = props {
2734 let mut hash = apr::hash::Hash::new(&pool);
2735 for (key, value) in props {
2736 let svn_str = crate::svn_string_helpers::svn_string_ncreate(value, &pool);
2737 unsafe {
2738 hash.insert(key.as_bytes(), svn_str as *mut std::ffi::c_void);
2739 }
2740 }
2741 unsafe { hash.as_mut_ptr() }
2742 } else {
2743 std::ptr::null_mut()
2744 };
2745
2746 let notify_baton = notify_func
2747 .map(|f| box_notify_baton_borrowed(f))
2748 .unwrap_or(std::ptr::null_mut());
2749
2750 let ret = unsafe {
2751 subversion_sys::svn_wc_add_from_disk3(
2752 self.ptr,
2753 path_cstr.as_ptr(),
2754 props_hash,
2755 skip_checks as i32,
2756 if notify_func.is_some() {
2757 Some(wrap_notify_func)
2758 } else {
2759 None
2760 },
2761 notify_baton,
2762 pool.as_mut_ptr(),
2763 )
2764 };
2765
2766 if !notify_baton.is_null() {
2767 unsafe { drop_notify_baton_borrowed(notify_baton) };
2768 }
2769
2770 Error::from_raw(ret)
2771 }
2772
2773 pub fn add_repos_file(
2794 &mut self,
2795 local_abspath: &std::path::Path,
2796 new_base_contents: &mut crate::io::Stream,
2797 new_contents: Option<&mut crate::io::Stream>,
2798 new_base_props: &std::collections::HashMap<String, Vec<u8>>,
2799 new_props: Option<&std::collections::HashMap<String, Vec<u8>>>,
2800 copyfrom_url: Option<&str>,
2801 copyfrom_rev: crate::Revnum,
2802 cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
2803 ) -> Result<(), Error<'static>> {
2804 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
2805 let copyfrom_url_cstr = copyfrom_url
2806 .map(|u| std::ffi::CString::new(u).expect("copyfrom_url must be valid UTF-8"));
2807
2808 let scratch_pool = apr::Pool::new();
2809
2810 let build_props_hash = |props: &std::collections::HashMap<String, Vec<u8>>,
2812 pool: &apr::Pool|
2813 -> *mut apr_sys::apr_hash_t {
2814 let mut hash = apr::hash::Hash::new(pool);
2815 for (name, value) in props {
2816 let svn_str = crate::svn_string_helpers::svn_string_ncreate(value, pool);
2817 unsafe {
2818 hash.insert(name.as_bytes(), svn_str as *mut std::ffi::c_void);
2819 }
2820 }
2821 unsafe { hash.as_mut_ptr() }
2822 };
2823
2824 let base_props_ptr = build_props_hash(new_base_props, &scratch_pool);
2825 let props_ptr: *mut apr_sys::apr_hash_t = match new_props {
2826 Some(p) => build_props_hash(p, &scratch_pool),
2827 None => std::ptr::null_mut(),
2828 };
2829
2830 let has_cancel = cancel_func.is_some();
2831 let cancel_baton = cancel_func
2832 .map(box_cancel_baton)
2833 .unwrap_or(std::ptr::null_mut());
2834
2835 let err = unsafe {
2836 let e = subversion_sys::svn_wc_add_repos_file4(
2837 self.ptr,
2838 path_cstr.as_ptr(),
2839 new_base_contents.as_mut_ptr(),
2840 new_contents
2841 .map(|s| s.as_mut_ptr())
2842 .unwrap_or(std::ptr::null_mut()),
2843 base_props_ptr,
2844 props_ptr,
2845 copyfrom_url_cstr
2846 .as_ref()
2847 .map_or(std::ptr::null(), |c| c.as_ptr()),
2848 copyfrom_rev.0,
2849 if has_cancel {
2850 Some(crate::wrap_cancel_func)
2851 } else {
2852 None
2853 },
2854 cancel_baton,
2855 scratch_pool.as_mut_ptr(),
2856 );
2857 if has_cancel && !cancel_baton.is_null() {
2858 drop_cancel_baton(cancel_baton);
2859 }
2860 e
2861 };
2862
2863 Error::from_raw(err)
2864 }
2865
2866 pub fn move_path(
2868 &mut self,
2869 src_abspath: &std::path::Path,
2870 dst_abspath: &std::path::Path,
2871 metadata_only: bool,
2872 _allow_mixed_revisions: bool,
2873 cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
2874 notify_func: Option<&dyn Fn(&Notify)>,
2875 ) -> Result<(), Error<'static>> {
2876 let src = src_abspath.to_str().unwrap();
2877 let src_cstr = crate::dirent::to_absolute_cstring(src)?;
2878 let dst = dst_abspath.to_str().unwrap();
2879 let dst_cstr = crate::dirent::to_absolute_cstring(dst)?;
2880 let pool = apr::Pool::new();
2881
2882 let cancel_baton = cancel_func
2883 .map(box_cancel_baton_borrowed)
2884 .unwrap_or(std::ptr::null_mut());
2885
2886 let notify_baton = notify_func
2887 .map(|f| box_notify_baton_borrowed(f))
2888 .unwrap_or(std::ptr::null_mut());
2889
2890 let ret = unsafe {
2891 subversion_sys::svn_wc_move(
2892 self.ptr,
2893 src_cstr.as_ptr(),
2894 dst_cstr.as_ptr(),
2895 metadata_only.into(),
2896 if cancel_func.is_some() {
2897 Some(crate::wrap_cancel_func)
2898 } else {
2899 None
2900 },
2901 cancel_baton,
2902 if notify_func.is_some() {
2903 Some(wrap_notify_func)
2904 } else {
2905 None
2906 },
2907 notify_baton,
2908 pool.as_mut_ptr(),
2909 )
2910 };
2911
2912 if !cancel_baton.is_null() {
2913 unsafe {
2914 drop(Box::from_raw(
2915 cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
2916 ))
2917 };
2918 }
2919 if !notify_baton.is_null() {
2920 unsafe { drop_notify_baton_borrowed(notify_baton) };
2921 }
2922
2923 Error::from_raw(ret)
2924 }
2925
2926 pub fn get_switch_editor<'s>(
2930 &'s mut self,
2931 anchor_abspath: &str,
2932 target_basename: &str,
2933 switch_url: &str,
2934 options: SwitchEditorOptions,
2935 ) -> Result<(crate::delta::WrapEditor<'s>, crate::Revnum), crate::Error<'s>> {
2936 let anchor_abspath_cstr = crate::dirent::to_absolute_cstring(anchor_abspath)?;
2937 let target_basename_cstr = std::ffi::CString::new(target_basename)?;
2938 let switch_url_cstr = std::ffi::CString::new(switch_url)?;
2939 let diff3_cmd_cstr = options.diff3_cmd.map(std::ffi::CString::new).transpose()?;
2940
2941 let result_pool = apr::Pool::new();
2942
2943 let preserved_exts_cstrs: Vec<std::ffi::CString> = options
2945 .preserved_exts
2946 .iter()
2947 .map(|&s| std::ffi::CString::new(s))
2948 .collect::<Result<Vec<_>, _>>()?;
2949 let preserved_exts_apr = if preserved_exts_cstrs.is_empty() {
2950 std::ptr::null()
2951 } else {
2952 let mut arr = apr::tables::TypedArray::<*const i8>::new(
2953 &result_pool,
2954 preserved_exts_cstrs.len() as i32,
2955 );
2956 for cstr in &preserved_exts_cstrs {
2957 arr.push(cstr.as_ptr());
2958 }
2959 unsafe { arr.as_ptr() }
2960 };
2961 let mut target_revision: subversion_sys::svn_revnum_t = 0;
2962 let mut editor_ptr: *const subversion_sys::svn_delta_editor_t = std::ptr::null();
2963 let mut edit_baton: *mut std::ffi::c_void = std::ptr::null_mut();
2964
2965 let has_fetch_dirents = options.fetch_dirents_func.is_some();
2967 let fetch_dirents_baton = options
2968 .fetch_dirents_func
2969 .map(|f| box_fetch_dirents_baton(f))
2970 .unwrap_or(std::ptr::null_mut());
2971 let has_conflict = options.conflict_func.is_some();
2972 let conflict_baton = options
2973 .conflict_func
2974 .map(|f| box_conflict_baton(f))
2975 .unwrap_or(std::ptr::null_mut());
2976 let has_external = options.external_func.is_some();
2977 let external_baton = options
2978 .external_func
2979 .map(|f| box_external_baton(f))
2980 .unwrap_or(std::ptr::null_mut());
2981 let has_cancel = options.cancel_func.is_some();
2982 let cancel_baton = options
2983 .cancel_func
2984 .map(box_cancel_baton)
2985 .unwrap_or(std::ptr::null_mut());
2986 let has_notify = options.notify_func.is_some();
2987 let notify_baton = options
2988 .notify_func
2989 .map(|f| box_notify_baton(f))
2990 .unwrap_or(std::ptr::null_mut());
2991
2992 let err = with_tmp_pool(|scratch_pool| unsafe {
2993 svn_result(subversion_sys::svn_wc_get_switch_editor4(
2994 &mut editor_ptr,
2995 &mut edit_baton,
2996 &mut target_revision,
2997 self.ptr,
2998 anchor_abspath_cstr.as_ptr(),
2999 target_basename_cstr.as_ptr(),
3000 switch_url_cstr.as_ptr(),
3001 if options.use_commit_times { 1 } else { 0 },
3002 options.depth.into(),
3003 if options.depth_is_sticky { 1 } else { 0 },
3004 if options.allow_unver_obstructions {
3005 1
3006 } else {
3007 0
3008 },
3009 if options.server_performs_filtering {
3010 1
3011 } else {
3012 0
3013 },
3014 diff3_cmd_cstr
3015 .as_ref()
3016 .map_or(std::ptr::null(), |c| c.as_ptr()),
3017 preserved_exts_apr,
3018 if has_fetch_dirents {
3019 Some(wrap_fetch_dirents_func)
3020 } else {
3021 None
3022 },
3023 fetch_dirents_baton,
3024 if has_conflict {
3025 Some(wrap_conflict_func)
3026 } else {
3027 None
3028 },
3029 conflict_baton,
3030 if has_external {
3031 Some(wrap_external_func)
3032 } else {
3033 None
3034 },
3035 external_baton,
3036 if has_cancel {
3037 Some(crate::wrap_cancel_func)
3038 } else {
3039 None
3040 },
3041 cancel_baton,
3042 if has_notify {
3043 Some(wrap_notify_func)
3044 } else {
3045 None
3046 },
3047 notify_baton,
3048 result_pool.as_mut_ptr(),
3049 scratch_pool.as_mut_ptr(),
3050 ))
3051 });
3052
3053 err?;
3054
3055 let mut batons = Vec::new();
3057 if !fetch_dirents_baton.is_null() {
3058 batons.push((
3059 fetch_dirents_baton,
3060 drop_fetch_dirents_baton as crate::delta::DropperFn,
3061 ));
3062 }
3063 if !conflict_baton.is_null() {
3064 batons.push((
3065 conflict_baton,
3066 drop_conflict_baton as crate::delta::DropperFn,
3067 ));
3068 }
3069 if !external_baton.is_null() {
3070 batons.push((
3071 external_baton,
3072 drop_external_baton as crate::delta::DropperFn,
3073 ));
3074 }
3075 if !cancel_baton.is_null() {
3076 batons.push((cancel_baton, drop_cancel_baton as crate::delta::DropperFn));
3077 }
3078 if !notify_baton.is_null() {
3079 batons.push((notify_baton, drop_notify_baton as crate::delta::DropperFn));
3080 }
3081
3082 let editor = crate::delta::WrapEditor {
3084 editor: editor_ptr,
3085 baton: edit_baton,
3086 _pool: apr::PoolHandle::owned(result_pool),
3087 callback_batons: batons,
3088 };
3089
3090 Ok((
3091 editor,
3092 crate::Revnum::from_raw(target_revision).unwrap_or_default(),
3093 ))
3094 }
3095
3096 pub fn get_diff_editor<'s>(
3102 &'s mut self,
3103 anchor_abspath: &str,
3104 target_abspath: &str,
3105 callbacks: &'s mut dyn DiffCallbacks,
3106 use_text_base: bool,
3107 depth: crate::Depth,
3108 ignore_ancestry: bool,
3109 show_copies_as_adds: bool,
3110 use_git_diff_format: bool,
3111 ) -> Result<crate::delta::WrapEditor<'s>, crate::Error<'s>> {
3112 let anchor_abspath_cstr = crate::dirent::to_absolute_cstring(anchor_abspath)?;
3113 let target_abspath_cstr = crate::dirent::to_absolute_cstring(target_abspath)?;
3114
3115 let result_pool = apr::Pool::new();
3116 let mut editor_ptr: *const subversion_sys::svn_delta_editor_t = std::ptr::null();
3117 let mut edit_baton: *mut std::ffi::c_void = std::ptr::null_mut();
3118
3119 let c_callbacks = Box::new(make_diff_callbacks4());
3121 let c_callbacks_ptr = &*c_callbacks as *const subversion_sys::svn_wc_diff_callbacks4_t;
3122
3123 let cb_baton: Box<*mut dyn DiffCallbacks> = Box::new(callbacks as *mut dyn DiffCallbacks);
3125 let cb_baton_ptr = &*cb_baton as *const *mut dyn DiffCallbacks as *mut std::ffi::c_void;
3126
3127 let err = with_tmp_pool(|scratch_pool| unsafe {
3128 svn_result(subversion_sys::svn_wc_get_diff_editor6(
3129 &mut editor_ptr,
3130 &mut edit_baton,
3131 self.ptr,
3132 anchor_abspath_cstr.as_ptr(),
3133 target_abspath_cstr.as_ptr(),
3134 depth.into(),
3135 if ignore_ancestry { 1 } else { 0 },
3136 if show_copies_as_adds { 1 } else { 0 },
3137 if use_git_diff_format { 1 } else { 0 },
3138 if use_text_base { 1 } else { 0 },
3139 0, 0, std::ptr::null(), c_callbacks_ptr,
3143 cb_baton_ptr,
3144 None, std::ptr::null_mut(), result_pool.as_mut_ptr(),
3147 scratch_pool.as_mut_ptr(),
3148 ))
3149 });
3150
3151 err?;
3152
3153 unsafe fn drop_callbacks(ptr: *mut std::ffi::c_void) {
3154 let _ = Box::from_raw(ptr as *mut subversion_sys::svn_wc_diff_callbacks4_t);
3155 }
3156 unsafe fn drop_baton(ptr: *mut std::ffi::c_void) {
3157 let _ = Box::from_raw(ptr as *mut *mut dyn DiffCallbacks);
3158 }
3159
3160 let batons: Vec<(*mut std::ffi::c_void, crate::delta::DropperFn)> = vec![
3161 (
3162 Box::into_raw(c_callbacks) as *mut std::ffi::c_void,
3163 drop_callbacks as crate::delta::DropperFn,
3164 ),
3165 (
3166 Box::into_raw(cb_baton) as *mut std::ffi::c_void,
3167 drop_baton as crate::delta::DropperFn,
3168 ),
3169 ];
3170
3171 let editor = crate::delta::WrapEditor {
3172 editor: editor_ptr,
3173 baton: edit_baton,
3174 _pool: apr::PoolHandle::owned(result_pool),
3175 callback_batons: batons,
3176 };
3177
3178 Ok(editor)
3179 }
3180
3181 pub fn delete(
3183 &mut self,
3184 local_abspath: &std::path::Path,
3185 keep_local: bool,
3186 delete_unversioned_target: bool,
3187 cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
3188 notify_func: Option<&dyn Fn(&Notify)>,
3189 ) -> Result<(), Error<'static>> {
3190 let path = local_abspath.to_str().unwrap();
3191 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3192 let pool = apr::Pool::new();
3193
3194 let cancel_baton = cancel_func
3195 .map(box_cancel_baton_borrowed)
3196 .unwrap_or(std::ptr::null_mut());
3197
3198 let notify_baton = notify_func
3199 .map(|f| box_notify_baton_borrowed(f))
3200 .unwrap_or(std::ptr::null_mut());
3201
3202 let ret = unsafe {
3203 subversion_sys::svn_wc_delete4(
3204 self.ptr,
3205 path_cstr.as_ptr(),
3206 keep_local.into(),
3207 delete_unversioned_target.into(),
3208 if cancel_func.is_some() {
3209 Some(crate::wrap_cancel_func)
3210 } else {
3211 None
3212 },
3213 cancel_baton,
3214 if notify_func.is_some() {
3215 Some(wrap_notify_func)
3216 } else {
3217 None
3218 },
3219 notify_baton,
3220 pool.as_mut_ptr(),
3221 )
3222 };
3223
3224 if !cancel_baton.is_null() {
3225 unsafe {
3226 drop(Box::from_raw(
3227 cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
3228 ))
3229 };
3230 }
3231 if !notify_baton.is_null() {
3232 unsafe { drop_notify_baton_borrowed(notify_baton) };
3233 }
3234
3235 Error::from_raw(ret)
3236 }
3237
3238 pub fn prop_get(
3243 &mut self,
3244 local_abspath: &std::path::Path,
3245 name: &str,
3246 ) -> Result<Option<Vec<u8>>, Error<'_>> {
3247 let path = local_abspath.to_str().unwrap();
3248 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3249 let name_cstr = std::ffi::CString::new(name).unwrap();
3250 let result_pool = apr::Pool::new();
3251 let scratch_pool = apr::Pool::new();
3252
3253 let mut value: *const subversion_sys::svn_string_t = std::ptr::null();
3254
3255 let err = unsafe {
3256 subversion_sys::svn_wc_prop_get2(
3257 &mut value,
3258 self.ptr,
3259 path_cstr.as_ptr(),
3260 name_cstr.as_ptr(),
3261 result_pool.as_mut_ptr(),
3262 scratch_pool.as_mut_ptr(),
3263 )
3264 };
3265
3266 Error::from_raw(err)?;
3267
3268 if value.is_null() {
3269 Ok(None)
3270 } else {
3271 Ok(Some(Vec::from(unsafe {
3272 std::slice::from_raw_parts((*value).data as *const u8, (*value).len)
3273 })))
3274 }
3275 }
3276
3277 pub fn prop_set(
3282 &mut self,
3283 local_abspath: &std::path::Path,
3284 name: &str,
3285 value: Option<&[u8]>,
3286 depth: crate::Depth,
3287 skip_checks: bool,
3288 changelist_filter: Option<&[&str]>,
3289 cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
3290 notify_func: Option<&dyn Fn(&Notify)>,
3291 ) -> Result<(), Error<'static>> {
3292 let path = local_abspath.to_str().unwrap();
3293 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3294 let name_cstr = std::ffi::CString::new(name).unwrap();
3295 let scratch_pool = apr::Pool::new();
3296
3297 let value_svn = value.map(|v| crate::string::BStr::from_bytes(v, &scratch_pool));
3299 let value_ptr = value_svn
3300 .as_ref()
3301 .map(|v| v.as_ptr())
3302 .unwrap_or(std::ptr::null());
3303
3304 let changelist_cstrings: Vec<_> = changelist_filter
3306 .map(|lists| {
3307 lists
3308 .iter()
3309 .map(|l| std::ffi::CString::new(*l).unwrap())
3310 .collect()
3311 })
3312 .unwrap_or_default();
3313
3314 let changelist_array = if changelist_filter.is_some() {
3315 let mut array = apr::tables::TypedArray::<*const i8>::new(
3316 &scratch_pool,
3317 changelist_cstrings.len() as i32,
3318 );
3319 for cstring in &changelist_cstrings {
3320 array.push(cstring.as_ptr());
3321 }
3322 unsafe { array.as_ptr() }
3323 } else {
3324 std::ptr::null()
3325 };
3326
3327 let cancel_baton = cancel_func
3328 .map(box_cancel_baton_borrowed)
3329 .unwrap_or(std::ptr::null_mut());
3330
3331 let notify_baton = notify_func
3332 .map(|f| box_notify_baton_borrowed(f))
3333 .unwrap_or(std::ptr::null_mut());
3334
3335 let err = unsafe {
3336 subversion_sys::svn_wc_prop_set4(
3337 self.ptr,
3338 path_cstr.as_ptr(),
3339 name_cstr.as_ptr(),
3340 value_ptr,
3341 depth.into(),
3342 skip_checks.into(),
3343 changelist_array,
3344 if cancel_func.is_some() {
3345 Some(crate::wrap_cancel_func)
3346 } else {
3347 None
3348 },
3349 cancel_baton,
3350 if notify_func.is_some() {
3351 Some(wrap_notify_func)
3352 } else {
3353 None
3354 },
3355 notify_baton,
3356 scratch_pool.as_mut_ptr(),
3357 )
3358 };
3359
3360 if !cancel_baton.is_null() {
3361 unsafe {
3362 drop(Box::from_raw(
3363 cancel_baton as *mut Box<dyn Fn() -> Result<(), Error<'static>>>,
3364 ))
3365 };
3366 }
3367 if !notify_baton.is_null() {
3368 unsafe { drop_notify_baton_borrowed(notify_baton) };
3369 }
3370
3371 Error::from_raw(err)
3372 }
3373
3374 pub fn prop_list(
3378 &mut self,
3379 local_abspath: &std::path::Path,
3380 ) -> Result<std::collections::HashMap<String, Vec<u8>>, Error<'_>> {
3381 let path = local_abspath.to_str().unwrap();
3382 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3383 let result_pool = apr::Pool::new();
3384 let scratch_pool = apr::Pool::new();
3385
3386 let mut props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
3387
3388 let err = unsafe {
3389 subversion_sys::svn_wc_prop_list2(
3390 &mut props,
3391 self.ptr,
3392 path_cstr.as_ptr(),
3393 result_pool.as_mut_ptr(),
3394 scratch_pool.as_mut_ptr(),
3395 )
3396 };
3397
3398 Error::from_raw(err)?;
3399
3400 let prop_hash = unsafe { crate::props::PropHash::from_ptr(props) };
3401 Ok(prop_hash.to_hashmap())
3402 }
3403
3404 pub fn get_prop_diffs(
3441 &mut self,
3442 local_abspath: &std::path::Path,
3443 ) -> Result<
3444 (
3445 Vec<PropChange>,
3446 Option<std::collections::HashMap<String, Vec<u8>>>,
3447 ),
3448 Error<'_>,
3449 > {
3450 let path = local_abspath.to_str().unwrap();
3451 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3452 let result_pool = apr::Pool::new();
3453 let scratch_pool = apr::Pool::new();
3454
3455 let mut propchanges: *mut apr::tables::apr_array_header_t = std::ptr::null_mut();
3456 let mut original_props: *mut apr::hash::apr_hash_t = std::ptr::null_mut();
3457
3458 let err = unsafe {
3459 subversion_sys::svn_wc_get_prop_diffs2(
3460 &mut propchanges,
3461 &mut original_props,
3462 self.ptr,
3463 path_cstr.as_ptr(),
3464 result_pool.as_mut_ptr(),
3465 scratch_pool.as_mut_ptr(),
3466 )
3467 };
3468
3469 Error::from_raw(err)?;
3470
3471 let changes = if propchanges.is_null() {
3474 Vec::new()
3475 } else {
3476 let array = unsafe {
3477 apr::tables::TypedArray::<subversion_sys::svn_prop_t>::from_ptr(propchanges)
3478 };
3479 array
3480 .iter()
3481 .map(|prop| unsafe {
3482 if prop.name.is_null() {
3483 panic!("Encountered null prop.name in propchanges array");
3484 }
3485 let name = std::ffi::CStr::from_ptr(prop.name)
3486 .to_str()
3487 .expect("Property name is not valid UTF-8")
3488 .to_owned();
3489 let value = if prop.value.is_null() {
3490 None
3491 } else {
3492 Some(crate::svn_string_helpers::to_vec(&*prop.value))
3493 };
3494 PropChange { name, value }
3495 })
3496 .collect()
3497 };
3498
3499 let original = if original_props.is_null() {
3501 None
3502 } else {
3503 let prop_hash = unsafe { crate::props::PropHash::from_ptr(original_props) };
3504 Some(prop_hash.to_hashmap())
3505 };
3506
3507 Ok((changes, original))
3508 }
3509
3510 pub fn read_kind(
3518 &mut self,
3519 local_abspath: &std::path::Path,
3520 show_deleted: bool,
3521 show_hidden: bool,
3522 ) -> Result<crate::NodeKind, Error<'static>> {
3523 let path = local_abspath.to_str().unwrap();
3524 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3525 let scratch_pool = apr::Pool::new();
3526
3527 let mut kind: subversion_sys::svn_node_kind_t =
3528 subversion_sys::svn_node_kind_t_svn_node_none;
3529
3530 let err = unsafe {
3531 subversion_sys::svn_wc_read_kind2(
3532 &mut kind,
3533 self.ptr,
3534 path_cstr.as_ptr(),
3535 show_deleted.into(),
3536 show_hidden.into(),
3537 scratch_pool.as_mut_ptr(),
3538 )
3539 };
3540
3541 Error::from_raw(err)?;
3542 Ok(kind.into())
3543 }
3544
3545 pub fn is_wc_root(&mut self, local_abspath: &std::path::Path) -> Result<bool, Error<'static>> {
3551 let path = local_abspath.to_str().unwrap();
3552 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3553 let scratch_pool = apr::Pool::new();
3554
3555 let mut wc_root: i32 = 0;
3556
3557 let err = unsafe {
3558 subversion_sys::svn_wc_is_wc_root2(
3559 &mut wc_root,
3560 self.ptr,
3561 path_cstr.as_ptr(),
3562 scratch_pool.as_mut_ptr(),
3563 )
3564 };
3565
3566 Error::from_raw(err)?;
3567 Ok(wc_root != 0)
3568 }
3569
3570 pub fn exclude(
3582 &mut self,
3583 local_abspath: &std::path::Path,
3584 cancel_func: Option<&dyn Fn() -> Result<(), Error<'static>>>,
3585 notify_func: Option<&dyn Fn(&Notify)>,
3586 ) -> Result<(), Error<'static>> {
3587 let path = local_abspath.to_str().unwrap();
3588 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
3589 let scratch_pool = apr::Pool::new();
3590
3591 let cancel_baton = cancel_func
3592 .map(box_cancel_baton_borrowed)
3593 .unwrap_or(std::ptr::null_mut());
3594
3595 let notify_baton = notify_func
3596 .map(|f| box_notify_baton_borrowed(f))
3597 .unwrap_or(std::ptr::null_mut());
3598
3599 let err = unsafe {
3600 subversion_sys::svn_wc_exclude(
3601 self.ptr,
3602 path_cstr.as_ptr(),
3603 if cancel_func.is_some() {
3604 Some(crate::wrap_cancel_func)
3605 } else {
3606 None
3607 },
3608 cancel_baton,
3609 if notify_func.is_some() {
3610 Some(wrap_notify_func)
3611 } else {
3612 None
3613 },
3614 notify_baton,
3615 scratch_pool.as_mut_ptr(),
3616 )
3617 };
3618
3619 if !cancel_baton.is_null() {
3621 unsafe {
3622 drop(Box::from_raw(
3623 cancel_baton as *mut &dyn Fn() -> Result<(), Error<'static>>,
3624 ));
3625 }
3626 }
3627 if !notify_baton.is_null() {
3628 unsafe {
3629 drop(Box::from_raw(notify_baton as *mut &dyn Fn(&Notify)));
3630 }
3631 }
3632
3633 Error::from_raw(err)
3634 }
3635
3636 pub fn diff(
3644 &mut self,
3645 target_abspath: &std::path::Path,
3646 options: &DiffOptions,
3647 callbacks: &mut dyn DiffCallbacks,
3648 ) -> Result<(), Error<'static>> {
3649 let target_cstr = crate::dirent::to_absolute_cstring(target_abspath)?;
3650
3651 let scratch_pool = apr::Pool::new();
3652
3653 let changelist_cstrs: Vec<std::ffi::CString> = options
3655 .changelists
3656 .iter()
3657 .map(|cl| std::ffi::CString::new(cl.as_str()).expect("changelist is valid UTF-8"))
3658 .collect();
3659 let mut changelist_arr =
3660 apr::tables::TypedArray::<*const i8>::new(&scratch_pool, changelist_cstrs.len() as i32);
3661 for s in &changelist_cstrs {
3662 changelist_arr.push(s.as_ptr());
3663 }
3664 let changelist_filter: *const apr_sys::apr_array_header_t =
3665 if options.changelists.is_empty() {
3666 std::ptr::null()
3667 } else {
3668 unsafe { changelist_arr.as_ptr() }
3669 };
3670
3671 let c_callbacks = make_diff_callbacks4();
3672
3673 let mut cb_ref: &mut dyn DiffCallbacks = callbacks;
3675 let baton = &mut cb_ref as *mut &mut dyn DiffCallbacks as *mut std::ffi::c_void;
3676
3677 with_tmp_pool(|scratch| unsafe {
3678 svn_result(subversion_sys::svn_wc_diff6(
3679 self.ptr,
3680 target_cstr.as_ptr(),
3681 &c_callbacks,
3682 baton,
3683 options.depth.into(),
3684 options.ignore_ancestry as i32,
3685 options.show_copies_as_adds as i32,
3686 options.use_git_diff_format as i32,
3687 changelist_filter,
3688 None, std::ptr::null_mut(), scratch.as_mut_ptr(),
3691 ))
3692 })
3693 }
3694
3695 pub fn merge(
3707 &mut self,
3708 left_abspath: &std::path::Path,
3709 right_abspath: &std::path::Path,
3710 target_abspath: &std::path::Path,
3711 left_label: Option<&str>,
3712 right_label: Option<&str>,
3713 target_label: Option<&str>,
3714 prop_diff: &[PropChange],
3715 options: &MergeOptions,
3716 ) -> Result<(MergeOutcome, NotifyState), Error<'static>> {
3717 let left_cstr = crate::dirent::to_absolute_cstring(left_abspath)?;
3718 let right_cstr = crate::dirent::to_absolute_cstring(right_abspath)?;
3719 let target_cstr = crate::dirent::to_absolute_cstring(target_abspath)?;
3720
3721 let left_label_cstr =
3722 left_label.map(|s| std::ffi::CString::new(s).expect("label must be valid UTF-8"));
3723 let right_label_cstr =
3724 right_label.map(|s| std::ffi::CString::new(s).expect("label must be valid UTF-8"));
3725 let target_label_cstr =
3726 target_label.map(|s| std::ffi::CString::new(s).expect("label must be valid UTF-8"));
3727
3728 let diff3_cstr = options
3729 .diff3_cmd
3730 .as_deref()
3731 .map(|s| std::ffi::CString::new(s).expect("diff3_cmd must be valid UTF-8"));
3732
3733 let scratch_pool = apr::Pool::new();
3734
3735 let prop_name_cstrs: Vec<std::ffi::CString> = prop_diff
3738 .iter()
3739 .map(|c| std::ffi::CString::new(c.name.as_str()).expect("prop name valid UTF-8"))
3740 .collect();
3741 let mut prop_diff_typed = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(
3742 &scratch_pool,
3743 prop_diff.len() as i32,
3744 );
3745 for (change, name_cstr) in prop_diff.iter().zip(prop_name_cstrs.iter()) {
3746 prop_diff_typed.push(subversion_sys::svn_prop_t {
3747 name: name_cstr.as_ptr(),
3748 value: if let Some(v) = &change.value {
3749 crate::svn_string_helpers::svn_string_ncreate(v, &scratch_pool)
3750 } else {
3751 std::ptr::null()
3752 },
3753 });
3754 }
3755 let prop_diff_arr: *const apr_sys::apr_array_header_t = if prop_diff.is_empty() {
3756 std::ptr::null()
3757 } else {
3758 unsafe { prop_diff_typed.as_ptr() }
3759 };
3760
3761 let merge_opt_cstrs: Vec<std::ffi::CString> = options
3764 .merge_options
3765 .iter()
3766 .map(|s| std::ffi::CString::new(s.as_str()).expect("merge option valid UTF-8"))
3767 .collect();
3768 let mut merge_opts_typed =
3769 apr::tables::TypedArray::<*const i8>::new(&scratch_pool, merge_opt_cstrs.len() as i32);
3770 for s in &merge_opt_cstrs {
3771 merge_opts_typed.push(s.as_ptr());
3772 }
3773 let merge_opts_arr: *const apr_sys::apr_array_header_t = if options.merge_options.is_empty()
3774 {
3775 std::ptr::null()
3776 } else {
3777 unsafe { merge_opts_typed.as_ptr() }
3778 };
3779
3780 let mut content_outcome = subversion_sys::svn_wc_merge_outcome_t_svn_wc_merge_no_merge;
3781 let mut props_state =
3782 subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable;
3783
3784 let err = unsafe {
3785 svn_result(subversion_sys::svn_wc_merge5(
3786 &mut content_outcome,
3787 &mut props_state,
3788 self.ptr,
3789 left_cstr.as_ptr(),
3790 right_cstr.as_ptr(),
3791 target_cstr.as_ptr(),
3792 left_label_cstr
3793 .as_ref()
3794 .map_or(std::ptr::null(), |s| s.as_ptr()),
3795 right_label_cstr
3796 .as_ref()
3797 .map_or(std::ptr::null(), |s| s.as_ptr()),
3798 target_label_cstr
3799 .as_ref()
3800 .map_or(std::ptr::null(), |s| s.as_ptr()),
3801 std::ptr::null(), std::ptr::null(), options.dry_run as i32,
3804 diff3_cstr.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
3805 merge_opts_arr,
3806 std::ptr::null_mut(), prop_diff_arr,
3808 None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
3813 ))
3814 };
3815 err?;
3816
3817 Ok((content_outcome.into(), props_state.into()))
3818 }
3819
3820 pub fn merge_props(
3841 &mut self,
3842 local_abspath: &std::path::Path,
3843 baseprops: Option<&std::collections::HashMap<String, Vec<u8>>>,
3844 propchanges: &[PropChange],
3845 dry_run: bool,
3846 conflict_func: Option<
3847 Box<
3848 dyn Fn(
3849 &crate::conflict::ConflictDescription,
3850 ) -> Result<crate::conflict::ConflictResult, Error<'static>>,
3851 >,
3852 >,
3853 cancel_func: Option<Box<dyn Fn() -> Result<(), Error<'static>>>>,
3854 ) -> Result<NotifyState, Error<'static>> {
3855 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
3856
3857 let scratch_pool = apr::Pool::new();
3858
3859 let baseprops_hash_ptr: *mut apr_sys::apr_hash_t = if let Some(props) = baseprops {
3861 let mut hash = apr::hash::Hash::new(&scratch_pool);
3862 let mut key_cstrs: Vec<std::ffi::CString> = Vec::with_capacity(props.len());
3864 let mut svn_strings: Vec<*mut subversion_sys::svn_string_t> =
3865 Vec::with_capacity(props.len());
3866 for (name, value) in props {
3867 key_cstrs
3868 .push(std::ffi::CString::new(name.as_str()).expect("prop name valid UTF-8"));
3869 svn_strings.push(crate::svn_string_helpers::svn_string_ncreate(
3870 value,
3871 &scratch_pool,
3872 ));
3873 }
3874 for (cstr, svn_str) in key_cstrs.iter().zip(svn_strings.iter()) {
3875 unsafe {
3876 hash.insert(cstr.as_bytes(), *svn_str as *mut std::ffi::c_void);
3877 }
3878 }
3879 unsafe { hash.as_mut_ptr() }
3880 } else {
3881 std::ptr::null_mut()
3882 };
3883
3884 let prop_name_cstrs: Vec<std::ffi::CString> = propchanges
3886 .iter()
3887 .map(|c| std::ffi::CString::new(c.name.as_str()).expect("prop name valid UTF-8"))
3888 .collect();
3889 let mut prop_changes_typed = apr::tables::TypedArray::<subversion_sys::svn_prop_t>::new(
3890 &scratch_pool,
3891 propchanges.len() as i32,
3892 );
3893 for (change, name_cstr) in propchanges.iter().zip(prop_name_cstrs.iter()) {
3894 prop_changes_typed.push(subversion_sys::svn_prop_t {
3895 name: name_cstr.as_ptr(),
3896 value: if let Some(v) = &change.value {
3897 crate::svn_string_helpers::svn_string_ncreate(v, &scratch_pool)
3898 } else {
3899 std::ptr::null()
3900 },
3901 });
3902 }
3903 let propchanges_arr: *const apr_sys::apr_array_header_t = if propchanges.is_empty() {
3904 std::ptr::null()
3905 } else {
3906 unsafe { prop_changes_typed.as_ptr() }
3907 };
3908
3909 let has_conflict = conflict_func.is_some();
3910 let conflict_baton = conflict_func
3911 .map(box_conflict_baton)
3912 .unwrap_or(std::ptr::null_mut());
3913 let has_cancel = cancel_func.is_some();
3914 let cancel_baton = cancel_func
3915 .map(box_cancel_baton)
3916 .unwrap_or(std::ptr::null_mut());
3917
3918 let mut state = subversion_sys::svn_wc_notify_state_t_svn_wc_notify_state_inapplicable;
3919
3920 let err = unsafe {
3921 let e = subversion_sys::svn_wc_merge_props3(
3922 &mut state,
3923 self.ptr,
3924 path_cstr.as_ptr(),
3925 std::ptr::null(), std::ptr::null(), baseprops_hash_ptr,
3928 propchanges_arr,
3929 dry_run as i32,
3930 if has_conflict {
3931 Some(wrap_conflict_func)
3932 } else {
3933 None
3934 },
3935 conflict_baton,
3936 if has_cancel {
3937 Some(crate::wrap_cancel_func)
3938 } else {
3939 None
3940 },
3941 cancel_baton,
3942 scratch_pool.as_mut_ptr(),
3943 );
3944 if has_conflict && !conflict_baton.is_null() {
3946 drop_conflict_baton(conflict_baton);
3947 }
3948 if has_cancel && !cancel_baton.is_null() {
3949 drop_cancel_baton(cancel_baton);
3950 }
3951 e
3952 };
3953
3954 Error::from_raw(err)?;
3955 Ok(state.into())
3956 }
3957
3958 pub fn revert(
3963 &mut self,
3964 local_abspath: &std::path::Path,
3965 options: &RevertOptions,
3966 ) -> Result<(), Error<'static>> {
3967 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
3968
3969 let scratch_pool = apr::Pool::new();
3970
3971 let changelist_cstrs: Vec<std::ffi::CString> = options
3973 .changelists
3974 .iter()
3975 .map(|cl| std::ffi::CString::new(cl.as_str()).expect("changelist is valid UTF-8"))
3976 .collect();
3977 let mut changelist_arr =
3978 apr::tables::TypedArray::<*const i8>::new(&scratch_pool, changelist_cstrs.len() as i32);
3979 for s in &changelist_cstrs {
3980 changelist_arr.push(s.as_ptr());
3981 }
3982 let changelist_filter: *const apr_sys::apr_array_header_t =
3983 if options.changelists.is_empty() {
3984 std::ptr::null()
3985 } else {
3986 unsafe { changelist_arr.as_ptr() }
3987 };
3988
3989 svn_result(unsafe {
3990 subversion_sys::svn_wc_revert6(
3991 self.ptr,
3992 path_cstr.as_ptr(),
3993 options.depth.into(),
3994 options.use_commit_times as i32,
3995 changelist_filter,
3996 options.clear_changelists as i32,
3997 options.metadata_only as i32,
3998 options.added_keep_local as i32,
3999 None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
4004 )
4005 })
4006 }
4007
4008 pub fn copy(
4025 &mut self,
4026 src_abspath: &std::path::Path,
4027 dst_abspath: &std::path::Path,
4028 metadata_only: bool,
4029 ) -> Result<(), Error<'static>> {
4030 let src_cstr = crate::dirent::to_absolute_cstring(src_abspath)?;
4031 let dst_cstr = crate::dirent::to_absolute_cstring(dst_abspath)?;
4032
4033 with_tmp_pool(|scratch| {
4034 svn_result(unsafe {
4035 subversion_sys::svn_wc_copy3(
4036 self.ptr,
4037 src_cstr.as_ptr(),
4038 dst_cstr.as_ptr(),
4039 metadata_only as i32,
4040 None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch.as_mut_ptr(),
4045 )
4046 })
4047 })
4048 }
4049
4050 pub fn set_changelist(
4063 &mut self,
4064 local_abspath: &std::path::Path,
4065 changelist: Option<&str>,
4066 depth: crate::Depth,
4067 changelist_filter: &[String],
4068 ) -> Result<(), Error<'static>> {
4069 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4070 let cl_cstr = changelist
4071 .map(|s| std::ffi::CString::new(s).expect("changelist name must be valid UTF-8"));
4072
4073 let scratch_pool = apr::Pool::new();
4074
4075 let filter_cstrs: Vec<std::ffi::CString> = changelist_filter
4076 .iter()
4077 .map(|s| std::ffi::CString::new(s.as_str()).expect("filter name must be valid UTF-8"))
4078 .collect();
4079 let mut filter_arr =
4080 apr::tables::TypedArray::<*const i8>::new(&scratch_pool, filter_cstrs.len() as i32);
4081 for s in &filter_cstrs {
4082 filter_arr.push(s.as_ptr());
4083 }
4084 let filter_ptr: *const apr_sys::apr_array_header_t = if changelist_filter.is_empty() {
4085 std::ptr::null()
4086 } else {
4087 unsafe { filter_arr.as_ptr() }
4088 };
4089
4090 svn_result(unsafe {
4091 subversion_sys::svn_wc_set_changelist2(
4092 self.ptr,
4093 path_cstr.as_ptr(),
4094 cl_cstr.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
4095 depth.into(),
4096 filter_ptr,
4097 None, std::ptr::null_mut(), None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
4102 )
4103 })
4104 }
4105
4106 pub fn get_changelists(
4119 &mut self,
4120 local_abspath: &std::path::Path,
4121 depth: crate::Depth,
4122 changelist_filter: &[String],
4123 mut callback: impl FnMut(&str, Option<&str>) -> Result<(), Error<'static>>,
4124 ) -> Result<(), Error<'static>> {
4125 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4126
4127 let scratch_pool = apr::Pool::new();
4128
4129 let filter_cstrs: Vec<std::ffi::CString> = changelist_filter
4130 .iter()
4131 .map(|s| std::ffi::CString::new(s.as_str()).expect("filter name must be valid UTF-8"))
4132 .collect();
4133 let mut filter_arr =
4134 apr::tables::TypedArray::<*const i8>::new(&scratch_pool, filter_cstrs.len() as i32);
4135 for s in &filter_cstrs {
4136 filter_arr.push(s.as_ptr());
4137 }
4138 let filter_ptr: *const apr_sys::apr_array_header_t = if changelist_filter.is_empty() {
4139 std::ptr::null()
4140 } else {
4141 unsafe { filter_arr.as_ptr() }
4142 };
4143
4144 unsafe extern "C" fn cl_callback(
4145 baton: *mut std::ffi::c_void,
4146 path: *const std::os::raw::c_char,
4147 changelist: *const std::os::raw::c_char,
4148 pool: *mut apr_sys::apr_pool_t,
4149 ) -> *mut subversion_sys::svn_error_t {
4150 let cb = &mut *(baton
4151 as *mut &mut dyn FnMut(&str, Option<&str>) -> Result<(), Error<'static>>);
4152 let local_path = subversion_sys::svn_dirent_local_style(path, pool);
4153 let path = std::ffi::CStr::from_ptr(local_path).to_str().unwrap_or("");
4154 let cl = if changelist.is_null() {
4155 None
4156 } else {
4157 Some(std::ffi::CStr::from_ptr(changelist).to_str().unwrap_or(""))
4158 };
4159 match cb(path, cl) {
4160 Ok(()) => std::ptr::null_mut(),
4161 Err(e) => e.into_raw(),
4162 }
4163 }
4164
4165 let mut cb_ref: &mut dyn FnMut(&str, Option<&str>) -> Result<(), Error<'static>> =
4166 &mut callback;
4167 let baton = &mut cb_ref
4168 as *mut &mut dyn FnMut(&str, Option<&str>) -> Result<(), Error<'static>>
4169 as *mut std::ffi::c_void;
4170
4171 svn_result(unsafe {
4172 subversion_sys::svn_wc_get_changelists(
4173 self.ptr,
4174 path_cstr.as_ptr(),
4175 depth.into(),
4176 filter_ptr,
4177 Some(cl_callback),
4178 baton,
4179 None, std::ptr::null_mut(), scratch_pool.as_mut_ptr(),
4182 )
4183 })
4184 }
4185
4186 pub fn status(
4193 &mut self,
4194 local_abspath: &std::path::Path,
4195 ) -> Result<Status<'static>, Error<'static>> {
4196 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4197 let result_pool = apr::Pool::new();
4198 let mut ptr: *mut subversion_sys::svn_wc_status3_t = std::ptr::null_mut();
4199 with_tmp_pool(|scratch| {
4200 svn_result(unsafe {
4201 subversion_sys::svn_wc_status3(
4202 &mut ptr,
4203 self.ptr,
4204 path_cstr.as_ptr(),
4205 result_pool.as_mut_ptr(),
4206 scratch.as_mut_ptr(),
4207 )
4208 })
4209 })?;
4210 Ok(Status {
4211 ptr,
4212 _pool: apr::pool::PoolHandle::owned(result_pool),
4213 })
4214 }
4215
4216 pub fn check_root(
4222 &mut self,
4223 local_abspath: &std::path::Path,
4224 ) -> Result<(bool, bool, crate::NodeKind), Error<'static>> {
4225 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4226 let mut is_wcroot: subversion_sys::svn_boolean_t = 0;
4227 let mut is_switched: subversion_sys::svn_boolean_t = 0;
4228 let mut kind: subversion_sys::svn_node_kind_t =
4229 subversion_sys::svn_node_kind_t_svn_node_unknown;
4230 with_tmp_pool(|scratch| {
4231 svn_result(unsafe {
4232 subversion_sys::svn_wc_check_root(
4233 &mut is_wcroot,
4234 &mut is_switched,
4235 &mut kind,
4236 self.ptr,
4237 path_cstr.as_ptr(),
4238 scratch.as_mut_ptr(),
4239 )
4240 })
4241 })?;
4242 Ok((is_wcroot != 0, is_switched != 0, kind.into()))
4243 }
4244
4245 pub fn restore(
4252 &mut self,
4253 local_abspath: &std::path::Path,
4254 use_commit_times: bool,
4255 ) -> Result<(), Error<'static>> {
4256 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4257 with_tmp_pool(|scratch| {
4258 svn_result(unsafe {
4259 subversion_sys::svn_wc_restore(
4260 self.ptr,
4261 path_cstr.as_ptr(),
4262 use_commit_times as i32,
4263 scratch.as_mut_ptr(),
4264 )
4265 })
4266 })
4267 }
4268
4269 pub fn get_ignores(
4277 &mut self,
4278 local_abspath: &std::path::Path,
4279 ) -> Result<Vec<String>, Error<'static>> {
4280 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4281 let result_pool = apr::Pool::new();
4282 let mut patterns: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
4283 with_tmp_pool(|scratch| {
4284 svn_result(unsafe {
4285 subversion_sys::svn_wc_get_ignores2(
4286 &mut patterns,
4287 self.ptr,
4288 path_cstr.as_ptr(),
4289 std::ptr::null_mut(), result_pool.as_mut_ptr(),
4291 scratch.as_mut_ptr(),
4292 )
4293 })
4294 })?;
4295 if patterns.is_null() {
4296 return Ok(Vec::new());
4297 }
4298 let result =
4299 unsafe { apr::tables::TypedArray::<*const std::os::raw::c_char>::from_ptr(patterns) }
4300 .iter()
4301 .map(|ptr| unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() })
4302 .collect();
4303 Ok(result)
4304 }
4305
4306 pub fn remove_from_revision_control(
4327 &mut self,
4328 local_abspath: &std::path::Path,
4329 destroy_wf: bool,
4330 instant_error: bool,
4331 ) -> Result<(), Error<'static>> {
4332 let path_cstr = crate::dirent::to_absolute_cstring(local_abspath)?;
4333 with_tmp_pool(|scratch| {
4334 svn_result(unsafe {
4335 subversion_sys::svn_wc_remove_from_revision_control2(
4336 self.ptr,
4337 path_cstr.as_ptr(),
4338 destroy_wf as i32,
4339 instant_error as i32,
4340 None, std::ptr::null_mut(), scratch.as_mut_ptr(),
4343 )
4344 })
4345 })
4346 }
4347}
4348
4349pub struct Notify {
4351 ptr: *const subversion_sys::svn_wc_notify_t,
4352}
4353
4354impl Notify {
4355 unsafe fn from_ptr(ptr: *const subversion_sys::svn_wc_notify_t) -> Self {
4356 Self { ptr }
4357 }
4358
4359 pub fn action(&self) -> u32 {
4361 unsafe { (*self.ptr).action as u32 }
4362 }
4363
4364 pub fn path(&self) -> Option<&str> {
4366 unsafe {
4367 if (*self.ptr).path.is_null() {
4368 None
4369 } else {
4370 Some(std::ffi::CStr::from_ptr((*self.ptr).path).to_str().unwrap())
4371 }
4372 }
4373 }
4374
4375 pub fn kind(&self) -> crate::NodeKind {
4377 unsafe { (*self.ptr).kind.into() }
4378 }
4379
4380 pub fn mime_type(&self) -> Option<&str> {
4382 unsafe {
4383 if (*self.ptr).mime_type.is_null() {
4384 None
4385 } else {
4386 Some(
4387 std::ffi::CStr::from_ptr((*self.ptr).mime_type)
4388 .to_str()
4389 .unwrap(),
4390 )
4391 }
4392 }
4393 }
4394
4395 pub fn lock(&self) -> Option<Lock> {
4397 unsafe {
4398 if (*self.ptr).lock.is_null() {
4399 None
4400 } else {
4401 Some(Lock::from_ptr((*self.ptr).lock))
4402 }
4403 }
4404 }
4405
4406 pub fn err(&self) -> Option<Error<'_>> {
4408 unsafe {
4409 if (*self.ptr).err.is_null() {
4410 None
4411 } else {
4412 Some(Error::from_raw((*self.ptr).err).unwrap_err())
4413 }
4414 }
4415 }
4416
4417 pub fn content_state(&self) -> u32 {
4419 unsafe { (*self.ptr).content_state as u32 }
4420 }
4421
4422 pub fn prop_state(&self) -> u32 {
4424 unsafe { (*self.ptr).prop_state as u32 }
4425 }
4426
4427 pub fn lock_state(&self) -> u32 {
4429 unsafe { (*self.ptr).lock_state as u32 }
4430 }
4431
4432 pub fn revision(&self) -> Option<crate::Revnum> {
4434 unsafe { crate::Revnum::from_raw((*self.ptr).revision) }
4435 }
4436
4437 pub fn changelist_name(&self) -> Option<&str> {
4439 unsafe {
4440 if (*self.ptr).changelist_name.is_null() {
4441 None
4442 } else {
4443 Some(
4444 std::ffi::CStr::from_ptr((*self.ptr).changelist_name)
4445 .to_str()
4446 .unwrap(),
4447 )
4448 }
4449 }
4450 }
4451
4452 pub fn url(&self) -> Option<&str> {
4454 unsafe {
4455 if (*self.ptr).url.is_null() {
4456 None
4457 } else {
4458 Some(std::ffi::CStr::from_ptr((*self.ptr).url).to_str().unwrap())
4459 }
4460 }
4461 }
4462
4463 pub fn path_prefix(&self) -> Option<&str> {
4465 unsafe {
4466 if (*self.ptr).path_prefix.is_null() {
4467 None
4468 } else {
4469 Some(
4470 std::ffi::CStr::from_ptr((*self.ptr).path_prefix)
4471 .to_str()
4472 .unwrap(),
4473 )
4474 }
4475 }
4476 }
4477
4478 pub fn prop_name(&self) -> Option<&str> {
4480 unsafe {
4481 if (*self.ptr).prop_name.is_null() {
4482 None
4483 } else {
4484 Some(
4485 std::ffi::CStr::from_ptr((*self.ptr).prop_name)
4486 .to_str()
4487 .unwrap(),
4488 )
4489 }
4490 }
4491 }
4492
4493 pub fn old_revision(&self) -> Option<crate::Revnum> {
4495 unsafe { crate::Revnum::from_raw((*self.ptr).old_revision) }
4496 }
4497
4498 pub fn hunk_original_start(&self) -> u64 {
4500 unsafe { (*self.ptr).hunk_original_start.into() }
4501 }
4502
4503 pub fn hunk_original_length(&self) -> u64 {
4505 unsafe { (*self.ptr).hunk_original_length.into() }
4506 }
4507
4508 pub fn hunk_modified_start(&self) -> u64 {
4510 unsafe { (*self.ptr).hunk_modified_start.into() }
4511 }
4512
4513 pub fn hunk_modified_length(&self) -> u64 {
4515 unsafe { (*self.ptr).hunk_modified_length.into() }
4516 }
4517
4518 pub fn hunk_matched_line(&self) -> u64 {
4520 unsafe { (*self.ptr).hunk_matched_line.into() }
4521 }
4522
4523 pub fn hunk_fuzz(&self) -> u64 {
4525 unsafe { (*self.ptr).hunk_fuzz.into() }
4526 }
4527}
4528
4529extern "C" fn wrap_conflict_func(
4531 result: *mut *mut subversion_sys::svn_wc_conflict_result_t,
4532 description: *const subversion_sys::svn_wc_conflict_description2_t,
4533 baton: *mut std::ffi::c_void,
4534 result_pool: *mut apr_sys::apr_pool_t,
4535 _scratch_pool: *mut apr_sys::apr_pool_t,
4536) -> *mut subversion_sys::svn_error_t {
4537 if baton.is_null() || description.is_null() || result.is_null() {
4538 return std::ptr::null_mut();
4539 }
4540
4541 let callback = unsafe {
4542 &*(baton
4543 as *const Box<
4544 dyn Fn(
4545 &crate::conflict::ConflictDescription,
4546 ) -> Result<crate::conflict::ConflictResult, Error<'static>>,
4547 >)
4548 };
4549
4550 let desc = match unsafe { crate::conflict::ConflictDescription::from_raw(description) } {
4552 Ok(d) => d,
4553 Err(mut e) => return unsafe { e.detach() },
4554 };
4555
4556 match callback(&desc) {
4557 Ok(conflict_result) => {
4558 unsafe {
4560 *result = conflict_result.to_raw(result_pool);
4561 }
4562 std::ptr::null_mut()
4563 }
4564 Err(mut e) => unsafe { e.detach() },
4565 }
4566}
4567
4568extern "C" fn wrap_external_func(
4570 baton: *mut std::ffi::c_void,
4571 local_abspath: *const i8,
4572 old_val: *const subversion_sys::svn_string_t,
4573 new_val: *const subversion_sys::svn_string_t,
4574 depth: subversion_sys::svn_depth_t,
4575 _scratch_pool: *mut apr_sys::apr_pool_t,
4576) -> *mut subversion_sys::svn_error_t {
4577 if baton.is_null() || local_abspath.is_null() {
4578 return std::ptr::null_mut();
4579 }
4580
4581 let callback = unsafe {
4582 &*(baton
4583 as *const Box<
4584 dyn Fn(
4585 &str,
4586 Option<&str>,
4587 Option<&str>,
4588 crate::Depth,
4589 ) -> Result<(), Error<'static>>,
4590 >)
4591 };
4592
4593 let path_str = unsafe {
4594 std::ffi::CStr::from_ptr(local_abspath)
4595 .to_str()
4596 .unwrap_or("")
4597 };
4598
4599 let old_str = if old_val.is_null() {
4600 None
4601 } else {
4602 unsafe {
4603 let data = (*old_val).data as *const u8;
4604 let len = (*old_val).len;
4605 std::str::from_utf8(std::slice::from_raw_parts(data, len)).ok()
4606 }
4607 };
4608
4609 let new_str = if new_val.is_null() {
4610 None
4611 } else {
4612 unsafe {
4613 let data = (*new_val).data as *const u8;
4614 let len = (*new_val).len;
4615 std::str::from_utf8(std::slice::from_raw_parts(data, len)).ok()
4616 }
4617 };
4618
4619 let depth_enum = crate::Depth::from(depth);
4620
4621 match callback(path_str, old_str, new_str, depth_enum) {
4622 Ok(()) => std::ptr::null_mut(),
4623 Err(mut e) => unsafe { e.detach() },
4624 }
4625}
4626
4627pub(crate) extern "C" fn wrap_notify_func(
4629 baton: *mut std::ffi::c_void,
4630 notify: *const subversion_sys::svn_wc_notify_t,
4631 _pool: *mut apr_sys::apr_pool_t,
4632) {
4633 if baton.is_null() || notify.is_null() {
4634 return;
4635 }
4636
4637 let callback = unsafe { &*(baton as *const Box<dyn Fn(&Notify)>) };
4638 let notify_struct = unsafe { Notify::from_ptr(notify) };
4639 callback(¬ify_struct);
4640}
4641
4642extern "C" fn wrap_fetch_dirents_func(
4644 baton: *mut std::ffi::c_void,
4645 dirents: *mut *mut apr_sys::apr_hash_t,
4646 repos_root_url: *const i8,
4647 repos_relpath: *const i8,
4648 result_pool: *mut apr_sys::apr_pool_t,
4649 _scratch_pool: *mut apr_sys::apr_pool_t,
4650) -> *mut subversion_sys::svn_error_t {
4651 if baton.is_null() || dirents.is_null() || repos_root_url.is_null() || repos_relpath.is_null() {
4652 return std::ptr::null_mut();
4653 }
4654
4655 let callback = unsafe {
4656 &*(baton
4657 as *const Box<
4658 dyn Fn(
4659 &str,
4660 &str,
4661 ) -> Result<
4662 std::collections::HashMap<String, crate::DirEntry>,
4663 Error<'static>,
4664 >,
4665 >)
4666 };
4667
4668 let root_url = unsafe {
4669 std::ffi::CStr::from_ptr(repos_root_url)
4670 .to_str()
4671 .unwrap_or("")
4672 };
4673
4674 let relpath = unsafe {
4675 std::ffi::CStr::from_ptr(repos_relpath)
4676 .to_str()
4677 .unwrap_or("")
4678 };
4679
4680 let pool = unsafe { apr::Pool::from_raw(result_pool) };
4681 match callback(root_url, relpath) {
4682 Ok(dirents_map) => {
4683 let mut hash = apr::hash::Hash::new(&pool);
4685 for (name, dirent) in dirents_map {
4686 let svn_dirent = pool.alloc::<subversion_sys::svn_dirent_t>();
4688 unsafe {
4689 let svn_dirent_ptr = (*svn_dirent).as_mut_ptr();
4690 std::ptr::write_bytes(svn_dirent_ptr, 0, 1);
4691 (*svn_dirent_ptr).kind = dirent.kind().into();
4692 (*svn_dirent_ptr).size = dirent.size();
4693 (*svn_dirent_ptr).has_props = if dirent.has_props() { 1 } else { 0 };
4694 (*svn_dirent_ptr).created_rev = dirent.created_rev().map(|r| r.0).unwrap_or(-1);
4695 (*svn_dirent_ptr).time = dirent.time().into();
4696 if let Some(author) = dirent.last_author() {
4697 (*svn_dirent_ptr).last_author = pool.pstrdup(author);
4698 }
4699 hash.insert(name.as_bytes(), svn_dirent_ptr as *mut std::ffi::c_void);
4700 }
4701 }
4702 unsafe {
4703 *dirents = hash.as_mut_ptr();
4704 }
4705 std::ptr::null_mut()
4706 }
4707 Err(mut e) => unsafe { e.detach() },
4708 }
4709}
4710
4711pub struct CommittedQueue {
4713 ptr: *mut subversion_sys::svn_wc_committed_queue_t,
4714 _pool: apr::Pool<'static>,
4715}
4716
4717impl Default for CommittedQueue {
4718 fn default() -> Self {
4719 Self::new()
4720 }
4721}
4722
4723impl CommittedQueue {
4724 pub fn new() -> Self {
4726 let pool = apr::Pool::new();
4727 let ptr = unsafe { subversion_sys::svn_wc_committed_queue_create(pool.as_mut_ptr()) };
4728 Self { ptr, _pool: pool }
4729 }
4730
4731 pub(crate) fn as_mut_ptr(&mut self) -> *mut subversion_sys::svn_wc_committed_queue_t {
4732 self.ptr
4733 }
4734}
4735
4736pub struct Lock {
4738 ptr: *const subversion_sys::svn_lock_t,
4739 _pool: Option<apr::Pool<'static>>,
4741}
4742
4743impl Lock {
4744 pub fn from_ptr(ptr: *const subversion_sys::svn_lock_t) -> Self {
4746 Self { ptr, _pool: None }
4747 }
4748
4749 pub fn new(path: Option<&str>, token: Option<&[u8]>) -> Self {
4751 let pool = apr::Pool::new();
4752 let lock_ptr = unsafe { subversion_sys::svn_lock_create(pool.as_mut_ptr()) };
4753 if let Some(p) = path {
4754 let cstr = std::ffi::CString::new(p).unwrap();
4755 unsafe {
4756 (*lock_ptr).path = apr_sys::apr_pstrdup(pool.as_mut_ptr(), cstr.as_ptr());
4757 }
4758 }
4759 if let Some(t) = token {
4760 let cstr = std::ffi::CString::new(t).unwrap();
4761 unsafe {
4762 (*lock_ptr).token = apr_sys::apr_pstrdup(pool.as_mut_ptr(), cstr.as_ptr());
4763 }
4764 }
4765 Self {
4766 ptr: lock_ptr as *const _,
4767 _pool: Some(pool),
4768 }
4769 }
4770
4771 pub fn as_ptr(&self) -> *const subversion_sys::svn_lock_t {
4773 self.ptr
4774 }
4775
4776 pub fn path(&self) -> Option<&str> {
4778 unsafe {
4779 let p = (*self.ptr).path;
4780 if p.is_null() {
4781 None
4782 } else {
4783 Some(std::ffi::CStr::from_ptr(p).to_str().unwrap())
4784 }
4785 }
4786 }
4787
4788 pub fn token(&self) -> Option<&str> {
4790 unsafe {
4791 let t = (*self.ptr).token;
4792 if t.is_null() {
4793 None
4794 } else {
4795 Some(std::ffi::CStr::from_ptr(t).to_str().unwrap())
4796 }
4797 }
4798 }
4799
4800 pub fn owner(&self) -> Option<&str> {
4802 unsafe {
4803 let o = (*self.ptr).owner;
4804 if o.is_null() {
4805 None
4806 } else {
4807 Some(std::ffi::CStr::from_ptr(o).to_str().unwrap())
4808 }
4809 }
4810 }
4811
4812 pub fn comment(&self) -> Option<&str> {
4814 unsafe {
4815 let c = (*self.ptr).comment;
4816 if c.is_null() {
4817 None
4818 } else {
4819 Some(std::ffi::CStr::from_ptr(c).to_str().unwrap())
4820 }
4821 }
4822 }
4823}
4824
4825pub fn cleanup(
4827 wc_path: &std::path::Path,
4828 break_locks: bool,
4829 fix_recorded_timestamps: bool,
4830 clear_dav_cache: bool,
4831 vacuum_pristines: bool,
4832 _include_externals: bool,
4833) -> Result<(), Error<'static>> {
4834 let path_cstr = crate::dirent::to_absolute_cstring(wc_path)?;
4835
4836 with_tmp_pool(|pool| -> Result<(), Error<'static>> {
4837 let mut ctx = std::ptr::null_mut();
4838 with_tmp_pool(|scratch_pool| {
4839 let err = unsafe {
4840 subversion_sys::svn_wc_context_create(
4841 &mut ctx,
4842 std::ptr::null_mut(),
4843 pool.as_mut_ptr(),
4844 scratch_pool.as_mut_ptr(),
4845 )
4846 };
4847 svn_result(err)
4848 })?;
4849
4850 let err = unsafe {
4851 subversion_sys::svn_wc_cleanup4(
4852 ctx,
4853 path_cstr.as_ptr(),
4854 break_locks as i32,
4855 fix_recorded_timestamps as i32,
4856 clear_dav_cache as i32,
4857 vacuum_pristines as i32,
4858 None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
4863 )
4864 };
4865 Error::from_raw(err)?;
4866 Ok(())
4867 })
4868}
4869
4870pub fn add(
4873 ctx: &mut Context,
4874 path: &std::path::Path,
4875 _depth: crate::Depth,
4876 force: bool,
4877 _no_ignore: bool,
4878 _no_autoprops: bool,
4879 _add_parents: bool,
4880) -> Result<(), Error<'static>> {
4881 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
4882
4883 with_tmp_pool(|pool| unsafe {
4884 let err = subversion_sys::svn_wc_add_from_disk3(
4885 ctx.as_mut_ptr(),
4886 path_cstr.as_ptr(),
4887 std::ptr::null_mut(), force as i32,
4889 None, std::ptr::null_mut(), pool.as_mut_ptr(),
4892 );
4893 Error::from_raw(err)
4894 })
4895}
4896
4897pub fn delete(
4899 ctx: &mut Context,
4900 path: &std::path::Path,
4901 keep_local: bool,
4902 delete_unversioned_target: bool,
4903) -> Result<(), Error<'static>> {
4904 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
4905
4906 with_tmp_pool(|pool| unsafe {
4907 let err = subversion_sys::svn_wc_delete4(
4908 ctx.as_mut_ptr(),
4909 path_cstr.as_ptr(),
4910 keep_local as i32,
4911 delete_unversioned_target as i32,
4912 None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
4917 );
4918 Error::from_raw(err)
4919 })
4920}
4921
4922pub fn revert(
4924 ctx: &mut Context,
4925 path: &std::path::Path,
4926 depth: crate::Depth,
4927 use_commit_times: bool,
4928 clear_changelists: bool,
4929 metadata_only: bool,
4930) -> Result<(), Error<'static>> {
4931 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
4932
4933 with_tmp_pool(|pool| unsafe {
4934 let err = subversion_sys::svn_wc_revert6(
4935 ctx.as_mut_ptr(),
4936 path_cstr.as_ptr(),
4937 depth.into(),
4938 use_commit_times as i32,
4939 std::ptr::null(), clear_changelists as i32,
4941 metadata_only as i32,
4942 1, None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
4948 );
4949 Error::from_raw(err)
4950 })
4951}
4952
4953pub fn copy_or_move(
4955 ctx: &mut Context,
4956 src: &std::path::Path,
4957 dst: &std::path::Path,
4958 is_move: bool,
4959 metadata_only: bool,
4960) -> Result<(), Error<'static>> {
4961 let src_cstr = crate::dirent::to_absolute_cstring(src)?;
4962 let dst_cstr = crate::dirent::to_absolute_cstring(dst)?;
4963
4964 with_tmp_pool(|pool| unsafe {
4965 if is_move {
4966 let err = subversion_sys::svn_wc_move(
4967 ctx.as_mut_ptr(),
4968 src_cstr.as_ptr(),
4969 dst_cstr.as_ptr(),
4970 metadata_only as i32,
4971 None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
4976 );
4977 Error::from_raw(err)
4978 } else {
4979 let err = subversion_sys::svn_wc_copy3(
4980 ctx.as_mut_ptr(),
4981 src_cstr.as_ptr(),
4982 dst_cstr.as_ptr(),
4983 metadata_only as i32,
4984 None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
4989 );
4990 Error::from_raw(err)
4991 }
4992 })
4993}
4994
4995pub fn resolve_conflict(
4997 ctx: &mut Context,
4998 path: &std::path::Path,
4999 depth: crate::Depth,
5000 resolve_text: bool,
5001 _resolve_props: bool,
5002 resolve_tree: bool,
5003 conflict_choice: ConflictChoice,
5004) -> Result<(), Error<'static>> {
5005 let path_cstr = crate::dirent::to_absolute_cstring(path)?;
5006
5007 with_tmp_pool(|pool| unsafe {
5008 let err = subversion_sys::svn_wc_resolved_conflict5(
5009 ctx.as_mut_ptr(),
5010 path_cstr.as_ptr(),
5011 depth.into(),
5012 resolve_text as i32,
5013 std::ptr::null(), resolve_tree as i32,
5015 conflict_choice.into(),
5016 None, std::ptr::null_mut(), None, std::ptr::null_mut(), pool.as_mut_ptr(),
5021 );
5022 Error::from_raw(err)
5023 })
5024}
5025
5026pub fn revision_status(
5030 wc_path: &std::path::Path,
5031 trail_url: Option<&str>,
5032 committed: bool,
5033) -> Result<(i64, i64, bool, bool), Error<'static>> {
5034 let path_cstr = crate::dirent::to_absolute_cstring(wc_path)?;
5035 let trail_cstr = trail_url.map(std::ffi::CString::new).transpose()?;
5036
5037 with_tmp_pool(|pool| -> Result<(i64, i64, bool, bool), Error<'static>> {
5038 let mut ctx = std::ptr::null_mut();
5039 with_tmp_pool(|scratch_pool| {
5040 let err = unsafe {
5041 subversion_sys::svn_wc_context_create(
5042 &mut ctx,
5043 std::ptr::null_mut(),
5044 pool.as_mut_ptr(),
5045 scratch_pool.as_mut_ptr(),
5046 )
5047 };
5048 svn_result(err)
5049 })?;
5050
5051 let mut status_ptr: *mut subversion_sys::svn_wc_revision_status_t = std::ptr::null_mut();
5052
5053 with_tmp_pool(|scratch_pool| {
5054 let err = unsafe {
5055 subversion_sys::svn_wc_revision_status2(
5056 &mut status_ptr,
5057 ctx,
5058 path_cstr.as_ptr(),
5059 trail_cstr.as_ref().map_or(std::ptr::null(), |c| c.as_ptr()),
5060 committed as i32,
5061 None, std::ptr::null_mut(), pool.as_mut_ptr(),
5064 scratch_pool.as_mut_ptr(),
5065 )
5066 };
5067 Error::from_raw(err)
5068 })?;
5069
5070 if status_ptr.is_null() {
5071 return Err(Error::from(std::io::Error::other(
5072 "Failed to get revision status",
5073 )));
5074 }
5075
5076 let status = unsafe { *status_ptr };
5077 Ok((
5078 status.min_rev.into(),
5079 status.max_rev.into(),
5080 status.switched != 0,
5081 status.modified != 0,
5082 ))
5083 })
5084}
5085
5086#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5093#[repr(i32)]
5094pub enum ConflictChoice {
5095 Postpone = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_postpone,
5097 Base = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_base,
5099 Theirs = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_full,
5101 Mine = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_full,
5103 TheirsConflict =
5105 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_conflict,
5106 MineConflict = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_conflict,
5108 Merged = subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_merged,
5110}
5111
5112impl From<ConflictChoice> for subversion_sys::svn_wc_conflict_choice_t {
5113 fn from(choice: ConflictChoice) -> Self {
5114 choice as subversion_sys::svn_wc_conflict_choice_t
5115 }
5116}
5117
5118#[derive(Debug, Clone)]
5123pub struct ExternalItem {
5124 pub target_dir: String,
5126 pub url: String,
5128 pub revision: crate::Revision,
5130 pub peg_revision: crate::Revision,
5132}
5133
5134pub fn parse_externals_description(
5139 parent_directory: &str,
5140 desc: &str,
5141 canonicalize_url: bool,
5142) -> Result<Vec<ExternalItem>, Error<'static>> {
5143 let pool = apr::Pool::new();
5144
5145 let parent_cstr = std::ffi::CString::new(parent_directory)
5146 .map_err(|_| Error::from_message("Invalid parent directory"))?;
5147 let desc_cstr = std::ffi::CString::new(desc)
5148 .map_err(|_| Error::from_message("Invalid externals description"))?;
5149
5150 unsafe {
5151 let mut externals_p: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
5152 let err = subversion_sys::svn_wc_parse_externals_description3(
5153 &mut externals_p,
5154 parent_cstr.as_ptr(),
5155 desc_cstr.as_ptr(),
5156 canonicalize_url.into(),
5157 pool.as_mut_ptr(),
5158 );
5159 svn_result(err)?;
5160
5161 if externals_p.is_null() {
5162 return Ok(Vec::new());
5163 }
5164
5165 let array =
5166 apr::tables::TypedArray::<*const subversion_sys::svn_wc_external_item2_t>::from_ptr(
5167 externals_p,
5168 );
5169
5170 let mut result = Vec::new();
5171 for item_ptr in array.iter() {
5172 let item = &*item_ptr;
5173
5174 let target_dir = if item.target_dir.is_null() {
5175 String::new()
5176 } else {
5177 std::ffi::CStr::from_ptr(item.target_dir)
5178 .to_str()
5179 .map_err(|_| Error::from_message("Invalid target_dir UTF-8"))?
5180 .to_string()
5181 };
5182
5183 let url = if item.url.is_null() {
5184 String::new()
5185 } else {
5186 std::ffi::CStr::from_ptr(item.url)
5187 .to_str()
5188 .map_err(|_| Error::from_message("Invalid url UTF-8"))?
5189 .to_string()
5190 };
5191
5192 unsafe fn convert_revision(
5193 rev: &subversion_sys::svn_opt_revision_t,
5194 ) -> crate::Revision {
5195 match rev.kind {
5196 subversion_sys::svn_opt_revision_kind_svn_opt_revision_unspecified => {
5197 crate::Revision::Unspecified
5198 }
5199 subversion_sys::svn_opt_revision_kind_svn_opt_revision_number => {
5200 crate::Revision::Number(crate::Revnum(*rev.value.number.as_ref()))
5201 }
5202 subversion_sys::svn_opt_revision_kind_svn_opt_revision_date => {
5203 crate::Revision::Date(*rev.value.date.as_ref())
5204 }
5205 subversion_sys::svn_opt_revision_kind_svn_opt_revision_head => {
5206 crate::Revision::Head
5207 }
5208 _ => crate::Revision::Unspecified,
5209 }
5210 }
5211
5212 result.push(ExternalItem {
5213 target_dir,
5214 url,
5215 revision: convert_revision(&item.revision),
5216 peg_revision: convert_revision(&item.peg_revision),
5217 });
5218 }
5219
5220 Ok(result)
5221 }
5222}
5223
5224pub fn get_default_ignores() -> Result<Vec<String>, crate::Error<'static>> {
5235 let result_pool = apr::Pool::new();
5236 let mut patterns: *mut apr_sys::apr_array_header_t = std::ptr::null_mut();
5237 svn_result(unsafe {
5238 subversion_sys::svn_wc_get_default_ignores(
5239 &mut patterns,
5240 std::ptr::null_mut(), result_pool.as_mut_ptr(),
5242 )
5243 })?;
5244 if patterns.is_null() {
5245 return Ok(Vec::new());
5246 }
5247 let result =
5248 unsafe { apr::tables::TypedArray::<*const std::os::raw::c_char>::from_ptr(patterns) }
5249 .iter()
5250 .map(|ptr| unsafe { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() })
5251 .collect();
5252 Ok(result)
5253}
5254
5255pub fn canonicalize_svn_prop(
5272 propname: &str,
5273 propval: &[u8],
5274 path: &str,
5275 kind: crate::NodeKind,
5276) -> Result<Vec<u8>, crate::Error<'static>> {
5277 let pool = apr::Pool::new();
5278 let propname_c = std::ffi::CString::new(propname)
5279 .map_err(|_| crate::Error::from_message("property name contains interior NUL"))?;
5280 let path_c = crate::dirent::canonicalize_path_or_url(path)
5281 .map_err(|_| crate::Error::from_message("path contains interior NUL"))?;
5282
5283 let input = crate::string::BStr::from_bytes(propval, &pool);
5284 let mut output: *const subversion_sys::svn_string_t = std::ptr::null();
5285
5286 svn_result(unsafe {
5287 subversion_sys::svn_wc_canonicalize_svn_prop(
5288 &mut output,
5289 propname_c.as_ptr(),
5290 input.as_ptr(),
5291 path_c.as_ptr(),
5292 kind.into(),
5293 1, None, std::ptr::null_mut(), pool.as_mut_ptr(),
5297 )
5298 })?;
5299
5300 if output.is_null() {
5301 return Ok(propval.to_vec());
5302 }
5303 let s = unsafe { &*output };
5304 Ok(unsafe { std::slice::from_raw_parts(s.data as *const u8, s.len).to_vec() })
5305}
5306
5307pub fn transmit_text_deltas<'a>(
5332 wc_ctx: &mut Context,
5333 local_abspath: &str,
5334 fulltext: bool,
5335 file_editor: &crate::delta::WrapFileEditor<'a>,
5336) -> Result<(String, String), crate::Error<'static>> {
5337 let result_pool = apr::Pool::new();
5338 let scratch_pool = apr::Pool::new();
5339
5340 let local_abspath_c = crate::dirent::to_absolute_cstring(local_abspath)?;
5341
5342 let mut md5_checksum: *const subversion_sys::svn_checksum_t = std::ptr::null();
5343 let mut sha1_checksum: *const subversion_sys::svn_checksum_t = std::ptr::null();
5344
5345 let (editor_ptr, baton_ptr) = file_editor.as_raw_parts();
5346
5347 let err = unsafe {
5348 subversion_sys::svn_wc_transmit_text_deltas3(
5349 &mut md5_checksum,
5350 &mut sha1_checksum,
5351 wc_ctx.ptr,
5352 local_abspath_c.as_ptr(),
5353 if fulltext { 1 } else { 0 },
5354 editor_ptr,
5355 baton_ptr,
5356 result_pool.as_mut_ptr(),
5357 scratch_pool.as_mut_ptr(),
5358 )
5359 };
5360 svn_result(err)?;
5361
5362 let md5_hex = if md5_checksum.is_null() {
5363 String::new()
5364 } else {
5365 let checksum = crate::Checksum::from_raw(md5_checksum);
5366 checksum.to_hex(&result_pool)
5367 };
5368
5369 let sha1_hex = if sha1_checksum.is_null() {
5370 String::new()
5371 } else {
5372 let checksum = crate::Checksum::from_raw(sha1_checksum);
5373 checksum.to_hex(&result_pool)
5374 };
5375
5376 Ok((md5_hex, sha1_hex))
5377}
5378
5379pub fn transmit_prop_deltas_file<'a>(
5389 wc_ctx: &mut Context,
5390 local_abspath: &str,
5391 file_editor: &crate::delta::WrapFileEditor<'a>,
5392) -> Result<(), crate::Error<'static>> {
5393 let scratch_pool = apr::Pool::new();
5394 let local_abspath_c = crate::dirent::to_absolute_cstring(local_abspath)?;
5395
5396 let (editor_ptr, baton_ptr) = file_editor.as_raw_parts();
5397
5398 let err = unsafe {
5399 subversion_sys::svn_wc_transmit_prop_deltas2(
5400 wc_ctx.ptr,
5401 local_abspath_c.as_ptr(),
5402 editor_ptr,
5403 baton_ptr,
5404 scratch_pool.as_mut_ptr(),
5405 )
5406 };
5407 svn_result(err)?;
5408
5409 Ok(())
5410}
5411
5412pub fn transmit_prop_deltas_dir<'a>(
5422 wc_ctx: &mut Context,
5423 local_abspath: &str,
5424 dir_editor: &crate::delta::WrapDirectoryEditor<'a>,
5425) -> Result<(), crate::Error<'static>> {
5426 let scratch_pool = apr::Pool::new();
5427 let local_abspath_c = crate::dirent::to_absolute_cstring(local_abspath)?;
5428
5429 let (editor_ptr, baton_ptr) = dir_editor.as_raw_parts();
5430
5431 let err = unsafe {
5432 subversion_sys::svn_wc_transmit_prop_deltas2(
5433 wc_ctx.ptr,
5434 local_abspath_c.as_ptr(),
5435 editor_ptr,
5436 baton_ptr,
5437 scratch_pool.as_mut_ptr(),
5438 )
5439 };
5440 svn_result(err)?;
5441
5442 Ok(())
5443}
5444
5445#[cfg(all(test, feature = "client", feature = "repos"))]
5446mod tests {
5447 use super::*;
5448 use std::path::{Path, PathBuf};
5449 use tempfile::tempdir;
5450
5451 fn ensure_timestamp_rollover() {
5463 std::thread::sleep(std::time::Duration::from_micros(2000));
5464 }
5465
5466 struct SvnTestFixture {
5468 pub _repos_path: PathBuf,
5469 pub wc_path: PathBuf,
5470 pub url: String,
5471 pub client_ctx: crate::client::Context,
5472 pub temp_dir: tempfile::TempDir,
5473 }
5474
5475 impl SvnTestFixture {
5476 fn new() -> Self {
5478 let temp_dir = tempfile::TempDir::new().unwrap();
5479 let repos_path = temp_dir.path().join("repos");
5480 let wc_path = temp_dir.path().join("wc");
5481
5482 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
5484
5485 let url = crate::path_to_file_url(&repos_path);
5487 let mut client_ctx = crate::client::Context::new().unwrap();
5488
5489 let uri = crate::uri::Uri::new(&url).unwrap();
5491 client_ctx
5492 .checkout(uri, &wc_path, &Self::default_checkout_options())
5493 .unwrap();
5494
5495 Self {
5496 _repos_path: repos_path,
5497 wc_path,
5498 url,
5499 client_ctx,
5500 temp_dir,
5501 }
5502 }
5503
5504 fn default_checkout_options() -> crate::client::CheckoutOptions {
5506 crate::client::CheckoutOptions {
5507 peg_revision: crate::Revision::Head,
5508 revision: crate::Revision::Head,
5509 depth: crate::Depth::Infinity,
5510 ignore_externals: false,
5511 allow_unver_obstructions: false,
5512 }
5513 }
5514
5515 fn add_file(&mut self, name: &str, content: &str) -> PathBuf {
5517 let file_path = self.wc_path.join(name);
5518 std::fs::write(&file_path, content).unwrap();
5519 self.client_ctx
5520 .add(&file_path, &crate::client::AddOptions::new())
5521 .unwrap();
5522 file_path
5523 }
5524
5525 fn add_dir(&mut self, name: &str) -> PathBuf {
5527 let dir_path = self.wc_path.join(name);
5528 std::fs::create_dir(&dir_path).unwrap();
5529 self.client_ctx
5530 .add(&dir_path, &crate::client::AddOptions::new())
5531 .unwrap();
5532 dir_path
5533 }
5534
5535 fn wc_path_str(&self) -> &str {
5537 self.wc_path
5538 .to_str()
5539 .expect("working copy path should be valid UTF-8")
5540 }
5541
5542 fn get_wc_url(&mut self) -> String {
5544 let wc_path = self
5545 .wc_path
5546 .to_str()
5547 .expect("path should be valid UTF-8")
5548 .to_string();
5549 let mut url = None;
5550 self.client_ctx
5551 .info(
5552 &wc_path,
5553 &crate::client::InfoOptions::default(),
5554 &|_, info| {
5555 url = Some(info.url().to_string());
5556 Ok(())
5557 },
5558 )
5559 .unwrap();
5560 url.expect("should have retrieved URL from info")
5561 }
5562
5563 fn commit(&mut self) {
5565 let wc_path_str = self.wc_path_str().to_string();
5566 let commit_opts = crate::client::CommitOptions::default();
5567 let revprops = std::collections::HashMap::new();
5568 self.client_ctx
5569 .commit(
5570 &[wc_path_str.as_str()],
5571 &commit_opts,
5572 revprops,
5573 None,
5574 &mut |_info| Ok(()),
5575 )
5576 .unwrap();
5577 }
5578 }
5579
5580 fn create_repo(base: &Path, name: &str) -> (PathBuf, String) {
5582 let repos_path = base.join(name);
5583 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
5584 let url = crate::path_to_file_url(&repos_path);
5585 (repos_path, url)
5586 }
5587
5588 #[test]
5589 fn test_context_creation() {
5590 let context = Context::new().unwrap();
5591 assert!(!context.ptr.is_null());
5592 }
5593
5594 #[test]
5595 fn test_context_close() {
5596 let mut context = Context::new().unwrap();
5597 assert!(!context.ptr.is_null());
5598 context.close();
5599 assert!(context.ptr.is_null());
5600 }
5601
5602 #[test]
5603 fn test_context_close_idempotent() {
5604 let mut context = Context::new().unwrap();
5605 context.close();
5606 context.close(); }
5608
5609 #[test]
5610 fn test_adm_dir_default() {
5611 let dir = get_adm_dir();
5613 assert_eq!(dir, ".svn");
5614 }
5615
5616 #[test]
5617 fn test_is_adm_dir() {
5618 assert!(is_adm_dir(".svn"));
5620
5621 assert!(!is_adm_dir("src"));
5623 assert!(!is_adm_dir("test"));
5624 assert!(!is_adm_dir(".git"));
5625 }
5626
5627 #[test]
5628 fn test_context_with_config() {
5629 let config = std::ptr::null_mut();
5631 Context::new_with_config(config).unwrap();
5632 }
5633
5634 #[test]
5635 fn test_check_wc() {
5636 let dir = tempdir().unwrap();
5637 let wc_path = dir.path();
5638
5639 let wc_format = check_wc(wc_path).unwrap();
5641 assert_eq!(wc_format, None);
5642 }
5643
5644 #[test]
5645 fn test_ensure_adm() {
5646 let dir = tempdir().unwrap();
5647 let wc_path = dir.path();
5648
5649 let result = ensure_adm(
5651 wc_path,
5652 "", "file:///test/repo", "file:///test/repo", 0, );
5657
5658 result.unwrap();
5659 }
5660
5661 #[test]
5664 fn test_text_modified() {
5665 let dir = tempdir().unwrap();
5666 let file_path = dir.path().join("test.txt");
5667 std::fs::write(&file_path, "test content").unwrap();
5668
5669 let result = text_modified(&file_path, false);
5671 assert!(result.is_err()); }
5673
5674 #[test]
5675 fn test_props_modified() {
5676 let dir = tempdir().unwrap();
5677 let file_path = dir.path().join("test.txt");
5678 std::fs::write(&file_path, "test content").unwrap();
5679
5680 let result = props_modified(&file_path);
5682 assert!(result.is_err()); }
5684
5685 #[test]
5686 fn test_status_enum() {
5687 assert_eq!(
5689 StatusKind::Normal as u32,
5690 subversion_sys::svn_wc_status_kind_svn_wc_status_normal as u32
5691 );
5692 assert_eq!(
5693 StatusKind::Added as u32,
5694 subversion_sys::svn_wc_status_kind_svn_wc_status_added as u32
5695 );
5696 assert_eq!(
5697 StatusKind::Deleted as u32,
5698 subversion_sys::svn_wc_status_kind_svn_wc_status_deleted as u32
5699 );
5700
5701 let status = StatusKind::from(subversion_sys::svn_wc_status_kind_svn_wc_status_modified);
5703 assert_eq!(status, StatusKind::Modified);
5704 }
5705
5706 #[test]
5707 fn test_schedule_enum() {
5708 assert_eq!(
5710 Schedule::Normal as u32,
5711 subversion_sys::svn_wc_schedule_t_svn_wc_schedule_normal as u32
5712 );
5713 assert_eq!(
5714 Schedule::Add as u32,
5715 subversion_sys::svn_wc_schedule_t_svn_wc_schedule_add as u32
5716 );
5717
5718 let schedule = Schedule::from(subversion_sys::svn_wc_schedule_t_svn_wc_schedule_delete);
5720 assert_eq!(schedule, Schedule::Delete);
5721 }
5722
5723 #[test]
5724 fn test_is_normal_prop() {
5725 assert!(is_normal_prop("svn:keywords"));
5728 assert!(is_normal_prop("svn:eol-style"));
5729 assert!(is_normal_prop("svn:mime-type"));
5730
5731 assert!(!is_normal_prop("svn:entry:committed-rev"));
5733 assert!(!is_normal_prop("svn:wc:ra_dav:version-url"));
5734 }
5735
5736 #[test]
5737 fn test_is_entry_prop() {
5738 assert!(is_entry_prop("svn:entry:committed-rev"));
5740 assert!(is_entry_prop("svn:entry:uuid"));
5741
5742 assert!(!is_entry_prop("svn:keywords"));
5744 assert!(!is_entry_prop("user:custom"));
5745 }
5746
5747 #[test]
5748 fn test_is_wc_prop() {
5749 assert!(is_wc_prop("svn:wc:ra_dav:version-url"));
5751
5752 assert!(!is_wc_prop("svn:keywords"));
5754 assert!(!is_wc_prop("user:custom"));
5755 }
5756
5757 #[test]
5758 fn test_conflict_choice_enum() {
5759 assert_eq!(
5761 ConflictChoice::Postpone as i32,
5762 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_postpone
5763 );
5764 assert_eq!(
5765 ConflictChoice::Base as i32,
5766 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_base
5767 );
5768 assert_eq!(
5769 ConflictChoice::Theirs as i32,
5770 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_full
5771 );
5772 assert_eq!(
5773 ConflictChoice::Mine as i32,
5774 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_full
5775 );
5776 }
5777
5778 #[test]
5779 fn test_crop_tree_basic() {
5780 let mut ctx = Context::new().unwrap();
5783 let tempdir = tempdir().unwrap();
5784
5785 let result = ctx.crop_tree(tempdir.path(), crate::Depth::Files, None);
5787
5788 assert!(result.is_err());
5790 }
5791
5792 #[test]
5793 fn test_resolved_conflict_basic() {
5794 let mut ctx = Context::new().unwrap();
5796 let tempdir = tempdir().unwrap();
5797
5798 let result = ctx.resolved_conflict(
5800 tempdir.path(),
5801 crate::Depth::Infinity,
5802 true, None, false, ConflictChoice::Mine,
5806 None,
5807 );
5808
5809 assert!(result.is_err());
5811 }
5812
5813 #[test]
5814 fn test_conflict_choice_conversion() {
5815 let choice = ConflictChoice::Mine;
5817 let svn_choice: subversion_sys::svn_wc_conflict_choice_t = choice.into();
5818 assert_eq!(
5819 svn_choice,
5820 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_mine_full
5821 );
5822
5823 let choice = ConflictChoice::Theirs;
5824 let svn_choice: subversion_sys::svn_wc_conflict_choice_t = choice.into();
5825 assert_eq!(
5826 svn_choice,
5827 subversion_sys::svn_wc_conflict_choice_t_svn_wc_conflict_choose_theirs_full
5828 );
5829 }
5830
5831 #[test]
5832 fn test_match_ignore_list() {
5833 assert!(match_ignore_list("foo", &["foo", "bar"]).unwrap());
5835 assert!(match_ignore_list("bar", &["foo", "bar"]).unwrap());
5836 assert!(!match_ignore_list("baz", &["foo", "bar"]).unwrap());
5837
5838 assert!(match_ignore_list("foo", &["f*"]).unwrap());
5840 assert!(match_ignore_list("foobar", &["f*"]).unwrap());
5841 assert!(!match_ignore_list("bar", &["f*"]).unwrap());
5842
5843 assert!(match_ignore_list("test.txt", &["*.txt"]).unwrap());
5845 assert!(match_ignore_list("file.txt", &["*.txt", "*.log"]).unwrap());
5846 assert!(!match_ignore_list("test.rs", &["*.txt"]).unwrap());
5847
5848 assert!(!match_ignore_list("foo", &[]).unwrap());
5850 }
5851
5852 #[test]
5853 fn test_add_from_disk() {
5854 let temp_dir = tempfile::tempdir().unwrap();
5857 let mut ctx = Context::new().unwrap();
5858
5859 let file_path = temp_dir.path().join("test.txt");
5860 std::fs::write(&file_path, "test content").unwrap();
5861
5862 let result = ctx.add_from_disk(&file_path, None, false, None);
5864 assert!(result.is_err());
5865 }
5866
5867 #[test]
5868 fn test_add_repos_file() {
5869 let temp_dir = tempfile::tempdir().unwrap();
5872 let mut ctx = Context::new().unwrap();
5873
5874 let file_path = temp_dir.path().join("newfile.txt");
5875
5876 let content = b"file content\n";
5877 let mut base_stream = crate::io::Stream::from(&content[..]);
5878 let base_props: std::collections::HashMap<String, Vec<u8>> =
5879 std::collections::HashMap::new();
5880
5881 let result = ctx.add_repos_file(
5882 &file_path,
5883 &mut base_stream,
5884 None,
5885 &base_props,
5886 None,
5887 None,
5888 crate::Revnum(-1),
5889 None,
5890 );
5891 assert!(result.is_err());
5893 }
5894
5895 #[test]
5896 fn test_move_path() {
5897 let temp_dir = tempfile::tempdir().unwrap();
5899 let mut ctx = Context::new().unwrap();
5900
5901 let src = temp_dir.path().join("src.txt");
5902 let dst = temp_dir.path().join("dst.txt");
5903 std::fs::write(&src, "content").unwrap();
5904
5905 let result = ctx.move_path(&src, &dst, false, false, None, None);
5907 assert!(result.is_err());
5908 }
5909
5910 #[test]
5911 fn test_delete() {
5912 let temp_dir = tempfile::tempdir().unwrap();
5914 let mut ctx = Context::new().unwrap();
5915
5916 let file_path = temp_dir.path().join("test.txt");
5917 std::fs::write(&file_path, "test content").unwrap();
5918
5919 let result = ctx.delete(&file_path, false, false, None, None);
5921 assert!(result.is_err());
5922 }
5923
5924 #[test]
5925 fn test_get_switch_editor() {
5926 let temp_dir = tempfile::tempdir().unwrap();
5928 let mut ctx = Context::new().unwrap();
5929
5930 let options = SwitchEditorOptions::new();
5932 let result = ctx.get_switch_editor(
5933 temp_dir.path().to_str().unwrap(),
5934 "",
5935 "http://example.com/repo/branches/test",
5936 options,
5937 );
5938
5939 assert!(result.is_err());
5941 }
5942
5943 #[test]
5944 fn test_get_switch_editor_api() {
5945 let temp_dir = tempfile::tempdir().unwrap();
5947 let mut ctx = Context::new().unwrap();
5948
5949 let options = SwitchEditorOptions::new();
5951 let result = ctx.get_switch_editor(
5952 temp_dir.path().to_str().unwrap(),
5953 "",
5954 "http://example.com/svn/trunk",
5955 options,
5956 );
5957
5958 assert!(result.is_err());
5960 }
5961
5962 #[test]
5963 fn test_get_switch_editor_with_target() {
5964 let temp_dir = tempfile::tempdir().unwrap();
5966 let mut ctx = Context::new().unwrap();
5967
5968 let options = SwitchEditorOptions {
5969 use_commit_times: true,
5970 depth: crate::Depth::Files,
5971 depth_is_sticky: true,
5972 allow_unver_obstructions: true,
5973 ..Default::default()
5974 };
5975 let result = ctx.get_switch_editor(
5976 temp_dir.path().to_str().unwrap(),
5977 "subdir", "http://example.com/svn/branches/test",
5979 options,
5980 );
5981
5982 assert!(result.is_err());
5983 }
5984
5985 #[test]
5986 fn test_get_diff_editor() {
5987 let temp_dir = tempfile::tempdir().unwrap();
5989 let wc_path = temp_dir.path().join("wc");
5990 std::fs::create_dir(&wc_path).unwrap();
5991
5992 ensure_adm(
5994 &wc_path,
5995 "test-uuid",
5996 "file:///tmp/test-repo",
5997 "file:///tmp/test-repo",
5998 0,
5999 )
6000 .unwrap();
6001
6002 let mut ctx = Context::new().unwrap();
6003 let mut callbacks = RecordingDiffCallbacks::new();
6004
6005 let result = ctx.get_diff_editor(
6006 wc_path.to_str().unwrap(),
6007 wc_path.to_str().unwrap(),
6008 &mut callbacks,
6009 false, crate::Depth::Infinity,
6011 false, false, false, );
6015
6016 result.unwrap();
6017 }
6018
6019 #[test]
6020 fn test_get_diff_editor_with_options() {
6021 let temp_dir = tempfile::tempdir().unwrap();
6023 let wc_path = temp_dir.path().join("wc");
6024 std::fs::create_dir(&wc_path).unwrap();
6025
6026 ensure_adm(
6028 &wc_path,
6029 "test-uuid",
6030 "file:///tmp/test-repo",
6031 "file:///tmp/test-repo",
6032 0,
6033 )
6034 .unwrap();
6035
6036 let mut ctx = Context::new().unwrap();
6037 let mut callbacks = RecordingDiffCallbacks::new();
6038
6039 let result = ctx.get_diff_editor(
6040 wc_path.to_str().unwrap(),
6041 wc_path.to_str().unwrap(),
6042 &mut callbacks,
6043 true, crate::Depth::Empty,
6045 true, true, true, );
6049
6050 result.unwrap();
6051 }
6052
6053 #[test]
6054 fn test_update_editor_trait() {
6055 use crate::delta::Editor;
6057
6058 fn check_editor_impl<T: Editor>() {}
6060
6061 check_editor_impl::<UpdateEditor<'_>>();
6063 }
6064
6065 #[test]
6066 fn test_committed_queue() {
6067 let queue = CommittedQueue::new();
6069 assert!(!queue.ptr.is_null());
6070
6071 let temp_dir = tempfile::tempdir().unwrap();
6073 let mut ctx = Context::new().unwrap();
6074 let mut queue = CommittedQueue::new();
6075
6076 let file_path = temp_dir.path().join("test.txt");
6077 std::fs::write(&file_path, "test content").unwrap();
6078
6079 let result = ctx.queue_committed(
6081 &file_path, false, true, &mut queue, None, false, false, None,
6082 );
6083 assert!(result.is_err());
6084 }
6085
6086 #[test]
6087 fn test_wc_prop_operations() {
6088 let mut fixture = SvnTestFixture::new();
6089 let file_path = fixture.add_file("test.txt", "test content");
6090
6091 fixture
6093 .client_ctx
6094 .propset(
6095 "test:property",
6096 Some(b"test value"),
6097 file_path.to_str().expect("file path should be valid UTF-8"),
6098 &crate::client::PropSetOptions::default(),
6099 )
6100 .unwrap();
6101
6102 let mut wc_ctx = Context::new().unwrap();
6104
6105 let value = wc_ctx.prop_get(&file_path, "test:property").unwrap();
6107 assert_eq!(value, Some(b"test value".to_vec()));
6108
6109 let missing = wc_ctx.prop_get(&file_path, "test:missing").unwrap();
6111 assert_eq!(missing, None);
6112
6113 let props = wc_ctx.prop_list(&file_path).unwrap();
6115 assert!(props.contains_key("test:property"));
6116 assert_eq!(props.get("test:property").unwrap(), b"test value");
6117 }
6118
6119 #[test]
6120 fn test_get_prop_diffs() {
6121 let mut fixture = SvnTestFixture::new();
6122 let file_path = fixture.add_file("test.txt", "test content");
6123
6124 fixture
6126 .client_ctx
6127 .propset(
6128 "test:prop1",
6129 Some(b"value1"),
6130 file_path.to_str().expect("file path should be valid UTF-8"),
6131 &crate::client::PropSetOptions::default(),
6132 )
6133 .unwrap();
6134
6135 let mut wc_ctx = Context::new().unwrap();
6137 let (changes, original) = wc_ctx.get_prop_diffs(&file_path).unwrap();
6138
6139 assert_eq!(changes.len(), 1);
6141 assert_eq!(changes[0].name, "test:prop1");
6142 assert_eq!(changes[0].value, Some(b"value1".to_vec()));
6143
6144 if let Some(orig) = original {
6146 assert!(!orig.contains_key("test:prop1"));
6147 }
6148 }
6149
6150 #[test]
6151 fn test_read_kind() {
6152 let mut fixture = SvnTestFixture::new();
6153 let file_path = fixture.add_file("test.txt", "test content");
6154 let dir_path = fixture.add_dir("subdir");
6155
6156 let mut wc_ctx = Context::new().unwrap();
6158
6159 let kind = wc_ctx.read_kind(&fixture.wc_path, false, false).unwrap();
6161 assert_eq!(kind, crate::NodeKind::Dir);
6162
6163 let kind = wc_ctx.read_kind(&file_path, false, false).unwrap();
6165 assert_eq!(kind, crate::NodeKind::File);
6166
6167 let kind = wc_ctx.read_kind(&dir_path, false, false).unwrap();
6169 assert_eq!(kind, crate::NodeKind::Dir);
6170
6171 let nonexistent = fixture.wc_path.join("nonexistent");
6173 let kind = wc_ctx.read_kind(&nonexistent, false, false).unwrap();
6174 assert_eq!(kind, crate::NodeKind::None);
6175 }
6176
6177 #[test]
6178 fn test_is_wc_root() {
6179 let mut fixture = SvnTestFixture::new();
6180 let subdir = fixture.add_dir("subdir");
6181
6182 let mut wc_ctx = Context::new().unwrap();
6184
6185 let is_root = wc_ctx.is_wc_root(&fixture.wc_path).unwrap();
6187 assert!(is_root, "Working copy root should be detected as WC root");
6188
6189 let is_root = wc_ctx.is_wc_root(&subdir).unwrap();
6191 assert!(!is_root, "Subdirectory should not be a WC root");
6192 }
6193
6194 #[test]
6195 fn test_get_pristine_contents() {
6196 use std::io::Read;
6197
6198 let mut fixture = SvnTestFixture::new();
6199
6200 let original_content = "original content";
6202 let file_path = fixture.add_file("test.txt", original_content);
6203 fixture.commit();
6204
6205 let modified_content = "modified content";
6207 std::fs::write(&file_path, modified_content).unwrap();
6208
6209 let mut wc_ctx = Context::new().unwrap();
6211 let pristine_stream = wc_ctx
6212 .get_pristine_contents(file_path.to_str().expect("file path should be valid UTF-8"))
6213 .unwrap();
6214
6215 assert!(
6217 pristine_stream.is_some(),
6218 "Should have pristine contents for committed file"
6219 );
6220
6221 let mut pristine_stream = pristine_stream.unwrap();
6223 let mut pristine_content = String::new();
6224 pristine_stream
6225 .read_to_string(&mut pristine_content)
6226 .unwrap();
6227
6228 assert_eq!(
6229 pristine_content, original_content,
6230 "Pristine content should match original"
6231 );
6232
6233 let new_file = fixture.add_file("new.txt", "new file content");
6235
6236 let pristine = wc_ctx
6237 .get_pristine_contents(
6238 new_file
6239 .to_str()
6240 .expect("new file path should be valid UTF-8"),
6241 )
6242 .unwrap();
6243 assert!(
6244 pristine.is_none(),
6245 "Newly added file should have no pristine contents"
6246 );
6247 }
6248
6249 #[test]
6250 fn test_exclude() {
6251 use std::cell::{Cell, RefCell};
6252 use tempfile::TempDir;
6253
6254 let temp_dir = TempDir::new().unwrap();
6256 let repos_path = temp_dir.path().join("repos");
6257 let wc_path = temp_dir.path().join("wc");
6258
6259 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
6261
6262 let url_str = crate::path_to_file_url(&repos_path);
6264 let url = crate::uri::Uri::new(&url_str).unwrap();
6265 let mut client_ctx = crate::client::Context::new().unwrap();
6266
6267 let checkout_opts = crate::client::CheckoutOptions {
6268 revision: crate::Revision::Head,
6269 peg_revision: crate::Revision::Head,
6270 depth: crate::Depth::Infinity,
6271 ignore_externals: false,
6272 allow_unver_obstructions: false,
6273 };
6274 client_ctx.checkout(url, &wc_path, &checkout_opts).unwrap();
6275
6276 let subdir = wc_path.join("subdir");
6278 std::fs::create_dir(&subdir).unwrap();
6279
6280 let file_in_subdir = subdir.join("file.txt");
6281 std::fs::write(&file_in_subdir, "content").unwrap();
6282
6283 client_ctx
6284 .add(&subdir, &crate::client::AddOptions::new())
6285 .unwrap();
6286
6287 let commit_opts = crate::client::CommitOptions::default();
6288 let revprops = std::collections::HashMap::new();
6289 client_ctx
6290 .commit(
6291 &[wc_path.to_str().unwrap()],
6292 &commit_opts,
6293 revprops,
6294 None,
6295 &mut |_info| Ok(()),
6296 )
6297 .unwrap();
6298
6299 assert!(subdir.exists(), "Subdirectory should exist before exclude");
6301 assert!(file_in_subdir.exists(), "File should exist before exclude");
6302
6303 let mut wc_ctx = Context::new().unwrap();
6305 let notifications = RefCell::new(Vec::new());
6306
6307 let result = wc_ctx.exclude(
6308 &subdir,
6309 None,
6310 Some(&|notify: &Notify| {
6311 notifications
6313 .borrow_mut()
6314 .push(format!("{:?}", notify.action()));
6315 }),
6316 );
6317
6318 assert!(result.is_ok(), "Exclude should succeed: {:?}", result);
6320
6321 assert!(
6323 !subdir.exists(),
6324 "Subdirectory should not exist after exclude"
6325 );
6326
6327 assert!(
6329 !notifications.borrow().is_empty(),
6330 "Should have received notifications"
6331 );
6332
6333 let kind = wc_ctx.read_kind(&subdir, false, false).unwrap();
6335 assert_eq!(
6336 kind,
6337 crate::NodeKind::None,
6338 "Excluded directory should show as None"
6339 );
6340
6341 let subdir2 = wc_path.join("subdir2");
6343 std::fs::create_dir(&subdir2).unwrap();
6344 client_ctx
6345 .add(&subdir2, &crate::client::AddOptions::new())
6346 .unwrap();
6347
6348 let commit_opts = crate::client::CommitOptions::default();
6349 let revprops = std::collections::HashMap::new();
6350 client_ctx
6351 .commit(
6352 &[wc_path.to_str().unwrap()],
6353 &commit_opts,
6354 revprops,
6355 None,
6356 &mut |_info| Ok(()),
6357 )
6358 .unwrap();
6359
6360 let cancel_called = Cell::new(false);
6362 let result = wc_ctx.exclude(
6363 &subdir2,
6364 Some(&|| {
6365 cancel_called.set(true);
6366 Ok(()) }),
6368 None,
6369 );
6370
6371 assert!(
6372 result.is_ok(),
6373 "Exclude with cancel callback should succeed"
6374 );
6375 assert!(
6376 cancel_called.get(),
6377 "Cancel callback should have been called"
6378 );
6379 assert!(!subdir2.exists(), "Second subdirectory should be excluded");
6380 }
6381
6382 #[test]
6383 fn test_get_pristine_props() {
6384 use tempfile::TempDir;
6385
6386 let temp_dir = TempDir::new().unwrap();
6388 let repos_path = temp_dir.path().join("repos");
6389 let wc_path = temp_dir.path().join("wc");
6390
6391 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
6393
6394 let url_str = crate::path_to_file_url(&repos_path);
6396 let url = crate::uri::Uri::new(&url_str).unwrap();
6397 let mut client_ctx = crate::client::Context::new().unwrap();
6398
6399 let checkout_opts = crate::client::CheckoutOptions {
6400 revision: crate::Revision::Head,
6401 peg_revision: crate::Revision::Head,
6402 depth: crate::Depth::Infinity,
6403 ignore_externals: false,
6404 allow_unver_obstructions: false,
6405 };
6406 client_ctx.checkout(url, &wc_path, &checkout_opts).unwrap();
6407
6408 let file_path = wc_path.join("test.txt");
6410 std::fs::write(&file_path, "original content").unwrap();
6411
6412 client_ctx
6413 .add(&file_path, &crate::client::AddOptions::new())
6414 .unwrap();
6415
6416 let propset_opts = crate::client::PropSetOptions::default();
6418 client_ctx
6419 .propset(
6420 "svn:eol-style",
6421 Some(b"native"),
6422 file_path.to_str().unwrap(),
6423 &propset_opts,
6424 )
6425 .unwrap();
6426 client_ctx
6427 .propset(
6428 "custom:prop",
6429 Some(b"custom value"),
6430 file_path.to_str().unwrap(),
6431 &propset_opts,
6432 )
6433 .unwrap();
6434
6435 let commit_opts = crate::client::CommitOptions::default();
6437 let revprops = std::collections::HashMap::new();
6438 client_ctx
6439 .commit(
6440 &[wc_path.to_str().unwrap()],
6441 &commit_opts,
6442 revprops,
6443 None,
6444 &mut |_info| Ok(()),
6445 )
6446 .unwrap();
6447
6448 let mut wc_ctx = Context::new().unwrap();
6450 let pristine_props = wc_ctx
6451 .get_pristine_props(file_path.to_str().unwrap())
6452 .unwrap();
6453
6454 assert!(
6455 pristine_props.is_some(),
6456 "Committed file should have pristine props"
6457 );
6458 let props = pristine_props.unwrap();
6459 assert!(
6460 props.contains_key("svn:eol-style"),
6461 "Should have svn:eol-style property"
6462 );
6463 assert_eq!(
6464 props.get("svn:eol-style").unwrap(),
6465 b"native",
6466 "svn:eol-style should be 'native'"
6467 );
6468 assert!(
6469 props.contains_key("custom:prop"),
6470 "Should have custom:prop property"
6471 );
6472 assert_eq!(
6473 props.get("custom:prop").unwrap(),
6474 b"custom value",
6475 "custom:prop should be 'custom value'"
6476 );
6477
6478 client_ctx
6480 .propset(
6481 "svn:eol-style",
6482 Some(b"LF"),
6483 file_path.to_str().unwrap(),
6484 &propset_opts,
6485 )
6486 .unwrap();
6487
6488 let pristine_props = wc_ctx
6490 .get_pristine_props(file_path.to_str().unwrap())
6491 .unwrap()
6492 .unwrap();
6493 assert_eq!(
6494 pristine_props.get("svn:eol-style").unwrap(),
6495 b"native",
6496 "Pristine svn:eol-style should still be 'native'"
6497 );
6498
6499 client_ctx
6501 .propset(
6502 "custom:prop",
6503 None,
6504 file_path.to_str().unwrap(),
6505 &propset_opts,
6506 )
6507 .unwrap();
6508
6509 let pristine_props = wc_ctx
6510 .get_pristine_props(file_path.to_str().unwrap())
6511 .unwrap()
6512 .unwrap();
6513 assert!(
6514 pristine_props.contains_key("custom:prop"),
6515 "Pristine props should still contain deleted property"
6516 );
6517
6518 let new_file = wc_path.join("new.txt");
6520 std::fs::write(&new_file, "new content").unwrap();
6521 client_ctx
6522 .add(&new_file, &crate::client::AddOptions::new())
6523 .unwrap();
6524
6525 let pristine = wc_ctx
6526 .get_pristine_props(new_file.to_str().unwrap())
6527 .unwrap();
6528 if let Some(props) = pristine {
6530 assert!(
6532 props.is_empty() || props.len() == 0,
6533 "Newly added file pristine props should be empty if not None"
6534 );
6535 }
6536
6537 let nonexistent = wc_path.join("nonexistent.txt");
6539 let result = wc_ctx.get_pristine_props(nonexistent.to_str().unwrap());
6540 assert!(result.is_err(), "Non-existent file should return an error");
6541 }
6542
6543 #[test]
6544 fn test_conflicted() {
6545 use tempfile::TempDir;
6546
6547 let temp_dir = TempDir::new().unwrap();
6549 let repos_path = temp_dir.path().join("repos");
6550 let wc1_path = temp_dir.path().join("wc1");
6551 let wc2_path = temp_dir.path().join("wc2");
6552
6553 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
6555
6556 let url_str = crate::path_to_file_url(&repos_path);
6557 let url = crate::uri::Uri::new(&url_str).unwrap();
6558
6559 let mut client_ctx1 = crate::client::Context::new().unwrap();
6561 let checkout_opts = crate::client::CheckoutOptions {
6562 revision: crate::Revision::Head,
6563 peg_revision: crate::Revision::Head,
6564 depth: crate::Depth::Infinity,
6565 ignore_externals: false,
6566 allow_unver_obstructions: false,
6567 };
6568 client_ctx1
6569 .checkout(url.clone(), &wc1_path, &checkout_opts)
6570 .unwrap();
6571
6572 let file_path1 = wc1_path.join("test.txt");
6574 std::fs::write(&file_path1, "line1\nline2\nline3\n").unwrap();
6575 client_ctx1
6576 .add(&file_path1, &crate::client::AddOptions::new())
6577 .unwrap();
6578
6579 let commit_opts = crate::client::CommitOptions::default();
6580 let revprops = std::collections::HashMap::new();
6581 client_ctx1
6582 .commit(
6583 &[wc1_path.to_str().unwrap()],
6584 &commit_opts,
6585 revprops.clone(),
6586 None,
6587 &mut |_info| Ok(()),
6588 )
6589 .unwrap();
6590
6591 let mut client_ctx2 = crate::client::Context::new().unwrap();
6593 client_ctx2
6594 .checkout(url.clone(), &wc2_path, &checkout_opts)
6595 .unwrap();
6596
6597 std::fs::write(&file_path1, "line1 modified in wc1\nline2\nline3\n").unwrap();
6599 client_ctx1
6600 .commit(
6601 &[wc1_path.to_str().unwrap()],
6602 &commit_opts,
6603 revprops.clone(),
6604 None,
6605 &mut |_info| Ok(()),
6606 )
6607 .unwrap();
6608
6609 let file_path2 = wc2_path.join("test.txt");
6611 std::fs::write(&file_path2, "line1 modified in wc2\nline2\nline3\n").unwrap();
6612
6613 ensure_timestamp_rollover();
6615
6616 let update_opts = crate::client::UpdateOptions::default();
6618 let _result = client_ctx2.update(
6619 &[wc2_path.to_str().unwrap()],
6620 crate::Revision::Head,
6621 &update_opts,
6622 );
6623 let mut wc_ctx = Context::new().unwrap();
6627 let (text_conflicted, prop_conflicted, tree_conflicted) =
6628 wc_ctx.conflicted(file_path2.to_str().unwrap()).unwrap();
6629
6630 assert!(
6631 text_conflicted,
6632 "File should have text conflict after conflicting update"
6633 );
6634 assert!(!prop_conflicted, "File should not have property conflict");
6635 assert!(!tree_conflicted, "File should not have tree conflict");
6636
6637 let propfile_path1 = wc1_path.join("proptest.txt");
6639 std::fs::write(&propfile_path1, "content").unwrap();
6640 client_ctx1
6641 .add(&propfile_path1, &crate::client::AddOptions::new())
6642 .unwrap();
6643
6644 let propset_opts = crate::client::PropSetOptions::default();
6645 client_ctx1
6646 .propset(
6647 "custom:prop",
6648 Some(b"value1"),
6649 propfile_path1.to_str().unwrap(),
6650 &propset_opts,
6651 )
6652 .unwrap();
6653
6654 client_ctx1
6655 .commit(
6656 &[wc1_path.to_str().unwrap()],
6657 &commit_opts,
6658 revprops.clone(),
6659 None,
6660 &mut |_info| Ok(()),
6661 )
6662 .unwrap();
6663
6664 let _result = client_ctx2.update(
6666 &[wc2_path.to_str().unwrap()],
6667 crate::Revision::Head,
6668 &update_opts,
6669 );
6670
6671 let propfile_path2 = wc2_path.join("proptest.txt");
6672 client_ctx2
6673 .propset(
6674 "custom:prop",
6675 Some(b"value2"),
6676 propfile_path2.to_str().unwrap(),
6677 &propset_opts,
6678 )
6679 .unwrap();
6680
6681 client_ctx1
6683 .propset(
6684 "custom:prop",
6685 Some(b"value1_modified"),
6686 propfile_path1.to_str().unwrap(),
6687 &propset_opts,
6688 )
6689 .unwrap();
6690 client_ctx1
6691 .commit(
6692 &[wc1_path.to_str().unwrap()],
6693 &commit_opts,
6694 revprops.clone(),
6695 None,
6696 &mut |_info| Ok(()),
6697 )
6698 .unwrap();
6699
6700 let _result = client_ctx2.update(
6702 &[wc2_path.to_str().unwrap()],
6703 crate::Revision::Head,
6704 &update_opts,
6705 );
6706
6707 let (_text_conflicted, prop_conflicted, _tree_conflicted) =
6708 wc_ctx.conflicted(propfile_path2.to_str().unwrap()).unwrap();
6709
6710 if prop_conflicted {
6712 assert!(
6713 prop_conflicted,
6714 "File should have property conflict after conflicting property update"
6715 );
6716 }
6717
6718 let clean_file = wc1_path.join("clean.txt");
6720 std::fs::write(&clean_file, "no conflicts here").unwrap();
6721 client_ctx1
6722 .add(&clean_file, &crate::client::AddOptions::new())
6723 .unwrap();
6724
6725 let (text_conflicted, prop_conflicted, tree_conflicted) =
6726 wc_ctx.conflicted(clean_file.to_str().unwrap()).unwrap();
6727
6728 assert!(!text_conflicted, "Clean file should not have text conflict");
6729 assert!(
6730 !prop_conflicted,
6731 "Clean file should not have property conflict"
6732 );
6733 assert!(!tree_conflicted, "Clean file should not have tree conflict");
6734
6735 let nonexistent = wc1_path.join("nonexistent.txt");
6737 let result = wc_ctx.conflicted(nonexistent.to_str().unwrap());
6738 assert!(result.is_err(), "Non-existent file should return an error");
6739 }
6740
6741 #[test]
6742 fn test_copy_or_move() {
6743 let td = tempfile::tempdir().unwrap();
6744 let repo_path = td.path().join("repo");
6745 let wc_path = td.path().join("wc");
6746
6747 crate::repos::Repos::create(&repo_path).unwrap();
6749
6750 let mut client_ctx = crate::client::Context::new().unwrap();
6752 let url_str = crate::path_to_file_url(&repo_path);
6753 let url = crate::uri::Uri::new(&url_str).unwrap();
6754
6755 client_ctx
6756 .checkout(
6757 url,
6758 &wc_path,
6759 &crate::client::CheckoutOptions {
6760 peg_revision: crate::Revision::Head,
6761 revision: crate::Revision::Head,
6762 depth: crate::Depth::Infinity,
6763 ignore_externals: false,
6764 allow_unver_obstructions: false,
6765 },
6766 )
6767 .unwrap();
6768
6769 let test_file = wc_path.join("test.txt");
6771 std::fs::write(&test_file, "test content").unwrap();
6772 client_ctx
6773 .add(&test_file, &crate::client::AddOptions::new())
6774 .unwrap();
6775
6776 let mut wc_ctx = Context::new().unwrap();
6778 let copy_dest = wc_path.join("test_copy.txt");
6779 let result = copy_or_move(&mut wc_ctx, &test_file, ©_dest, false, false);
6780
6781 assert!(result.is_err());
6783
6784 let move_dest = wc_path.join("test_moved.txt");
6786 let result = copy_or_move(&mut wc_ctx, &test_file, &move_dest, true, false);
6787
6788 assert!(result.is_err());
6790 }
6791
6792 #[test]
6793 fn test_revert() {
6794 let td = tempfile::tempdir().unwrap();
6795 let repo_path = td.path().join("repo");
6796 let wc_path = td.path().join("wc");
6797
6798 crate::repos::Repos::create(&repo_path).unwrap();
6800
6801 let mut client_ctx = crate::client::Context::new().unwrap();
6803 let url_str = crate::path_to_file_url(&repo_path);
6804 let url = crate::uri::Uri::new(&url_str).unwrap();
6805
6806 client_ctx
6807 .checkout(
6808 url,
6809 &wc_path,
6810 &crate::client::CheckoutOptions {
6811 peg_revision: crate::Revision::Head,
6812 revision: crate::Revision::Head,
6813 depth: crate::Depth::Infinity,
6814 ignore_externals: false,
6815 allow_unver_obstructions: false,
6816 },
6817 )
6818 .unwrap();
6819
6820 let test_file = wc_path.join("test.txt");
6822 std::fs::write(&test_file, "original content").unwrap();
6823 client_ctx
6824 .add(&test_file, &crate::client::AddOptions::new())
6825 .unwrap();
6826
6827 let mut wc_ctx = Context::new().unwrap();
6829 let result = revert(
6830 &mut wc_ctx,
6831 &test_file,
6832 crate::Depth::Empty,
6833 false,
6834 false,
6835 false,
6836 );
6837
6838 assert!(result.is_err());
6840 }
6841
6842 #[test]
6843 fn test_cleanup() {
6844 let td = tempfile::tempdir().unwrap();
6845 let repo_path = td.path().join("repo");
6846 let wc_path = td.path().join("wc");
6847
6848 crate::repos::Repos::create(&repo_path).unwrap();
6850
6851 let mut client_ctx = crate::client::Context::new().unwrap();
6853 let url_str = crate::path_to_file_url(&repo_path);
6854 let url = crate::uri::Uri::new(&url_str).unwrap();
6855
6856 client_ctx
6857 .checkout(
6858 url,
6859 &wc_path,
6860 &crate::client::CheckoutOptions {
6861 peg_revision: crate::Revision::Head,
6862 revision: crate::Revision::Head,
6863 depth: crate::Depth::Infinity,
6864 ignore_externals: false,
6865 allow_unver_obstructions: false,
6866 },
6867 )
6868 .unwrap();
6869
6870 let result = cleanup(&wc_path, false, false, false, false, false);
6872
6873 result.unwrap();
6875
6876 cleanup(&wc_path, true, false, false, false, false).unwrap();
6878
6879 cleanup(&wc_path, false, true, false, false, false).unwrap();
6881 }
6882
6883 #[test]
6884 fn test_get_actual_target() {
6885 let td = tempfile::tempdir().unwrap();
6886 let repo_path = td.path().join("repo");
6887 let wc_path = td.path().join("wc");
6888
6889 crate::repos::Repos::create(&repo_path).unwrap();
6891
6892 let mut client_ctx = crate::client::Context::new().unwrap();
6894 let url_str = crate::path_to_file_url(&repo_path);
6895 let url = crate::uri::Uri::new(&url_str).unwrap();
6896
6897 client_ctx
6898 .checkout(
6899 url,
6900 &wc_path,
6901 &crate::client::CheckoutOptions {
6902 peg_revision: crate::Revision::Head,
6903 revision: crate::Revision::Head,
6904 depth: crate::Depth::Infinity,
6905 ignore_externals: false,
6906 allow_unver_obstructions: false,
6907 },
6908 )
6909 .unwrap();
6910
6911 let (anchor, target) = get_actual_target(&wc_path).unwrap();
6913 assert!(!anchor.is_empty() || !target.is_empty());
6915 }
6916
6917 #[test]
6918 fn test_walk_status() {
6919 let td = tempfile::tempdir().unwrap();
6920 let repo_path = td.path().join("repo");
6921 let wc_path = td.path().join("wc");
6922
6923 crate::repos::Repos::create(&repo_path).unwrap();
6925
6926 let mut client_ctx = crate::client::Context::new().unwrap();
6928 let url_str = crate::path_to_file_url(&repo_path);
6929 let url = crate::uri::Uri::new(&url_str).unwrap();
6930
6931 client_ctx
6932 .checkout(
6933 url,
6934 &wc_path,
6935 &crate::client::CheckoutOptions {
6936 peg_revision: crate::Revision::Head,
6937 revision: crate::Revision::Head,
6938 depth: crate::Depth::Infinity,
6939 ignore_externals: false,
6940 allow_unver_obstructions: false,
6941 },
6942 )
6943 .unwrap();
6944
6945 let test_file = wc_path.join("test.txt");
6947 std::fs::write(&test_file, "test content").unwrap();
6948 client_ctx
6949 .add(&test_file, &crate::client::AddOptions::new())
6950 .unwrap();
6951
6952 let mut wc_ctx = Context::new().unwrap();
6954 let mut status_count = 0;
6955 let result = wc_ctx.walk_status(
6956 &wc_path,
6957 crate::Depth::Infinity,
6958 true, false, false, None, |_path, _status| {
6963 status_count += 1;
6964 Ok(())
6965 },
6966 );
6967
6968 result.unwrap();
6969 assert!(status_count >= 1, "Should have at least one status entry");
6971 }
6972
6973 #[test]
6974 fn test_wc_version() {
6975 let version = version();
6976 assert!(version.major() > 0);
6977 }
6978
6979 #[test]
6984 #[ignore]
6985 fn test_set_and_get_adm_dir() {
6986 set_adm_dir("_svn").unwrap();
6988
6989 let dir = get_adm_dir();
6990 assert_eq!(dir, "_svn");
6991
6992 set_adm_dir(".svn").unwrap();
6994
6995 let dir = get_adm_dir();
6996 assert_eq!(dir, ".svn");
6997 }
6998
6999 #[test]
7000 fn test_context_add() {
7001 let fixture = SvnTestFixture::new();
7002
7003 let new_file = fixture.wc_path.join("newfile.txt");
7005 std::fs::write(&new_file, b"test content").unwrap();
7006
7007 let mut wc_ctx = Context::new().unwrap();
7011 let new_file_abs = new_file.canonicalize().unwrap();
7012
7013 let result = wc_ctx.add(
7014 new_file_abs
7015 .to_str()
7016 .expect("file path should be valid UTF-8"),
7017 crate::Depth::Infinity,
7018 None,
7019 None,
7020 );
7021
7022 assert!(result.is_err());
7026 let err_str = format!("{:?}", result.err().unwrap());
7027 assert!(
7028 err_str.to_lowercase().contains("lock"),
7029 "Expected lock error, got: {}",
7030 err_str
7031 );
7032 }
7033
7034 #[test]
7035 fn test_context_relocate() {
7036 let tmp_dir = tempfile::tempdir().unwrap();
7037 let repos_path = tmp_dir.path().join("repo");
7038 let wc_path = tmp_dir.path().join("wc");
7039
7040 crate::repos::Repos::create(&repos_path).unwrap();
7042 let url_str = crate::path_to_file_url(&repos_path);
7043 let url = crate::uri::Uri::new(&url_str).unwrap();
7044
7045 let mut client_ctx = crate::client::Context::new().unwrap();
7047 client_ctx
7048 .checkout(
7049 url,
7050 &wc_path,
7051 &crate::client::CheckoutOptions {
7052 peg_revision: crate::Revision::Head,
7053 revision: crate::Revision::Head,
7054 depth: crate::Depth::Infinity,
7055 ignore_externals: false,
7056 allow_unver_obstructions: false,
7057 },
7058 )
7059 .unwrap();
7060
7061 let repos_path2 = tmp_dir.path().join("repo_moved");
7063 std::fs::rename(&repos_path, &repos_path2).unwrap();
7064 let repos_url2 = crate::path_to_file_url(&repos_path2);
7065
7066 let mut wc_ctx = Context::new().unwrap();
7068 let result = wc_ctx.relocate(wc_path.to_str().unwrap(), &url_str, &repos_url2);
7069
7070 assert!(result.is_ok(), "relocate() failed: {:?}", result.err());
7072 }
7073
7074 #[test]
7075 fn test_context_upgrade() {
7076 let fixture = SvnTestFixture::new();
7077
7078 let mut wc_ctx = Context::new().unwrap();
7080 let result = wc_ctx.upgrade(fixture.wc_path_str());
7081 assert!(result.is_ok(), "upgrade() failed: {:?}", result.err());
7082 }
7083
7084 #[test]
7085 fn test_get_update_editor4_with_callbacks() {
7086 let tmp_dir = tempfile::tempdir().unwrap();
7088 let repos_path = tmp_dir.path().join("repo");
7089 let wc_path = tmp_dir.path().join("wc");
7090
7091 crate::repos::Repos::create(&repos_path).unwrap();
7093 let url_str = crate::path_to_file_url(&repos_path);
7094 let url = crate::uri::Uri::new(&url_str).unwrap();
7095
7096 let mut client_ctx = crate::client::Context::new().unwrap();
7098 client_ctx
7099 .checkout(
7100 url,
7101 &wc_path,
7102 &crate::client::CheckoutOptions {
7103 peg_revision: crate::Revision::Head,
7104 revision: crate::Revision::Head,
7105 depth: crate::Depth::Infinity,
7106 ignore_externals: false,
7107 allow_unver_obstructions: false,
7108 },
7109 )
7110 .unwrap();
7111
7112 let cancel_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
7114 let cancel_called_clone = cancel_called.clone();
7115
7116 let mut wc_ctx = Context::new().unwrap();
7117 let options = UpdateEditorOptions {
7118 cancel_func: Some(Box::new(move || {
7119 cancel_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
7120 Ok(())
7121 })),
7122 ..Default::default()
7123 };
7124 let result = get_update_editor4(&mut wc_ctx, wc_path.to_str().unwrap(), "", options);
7125
7126 assert!(
7127 result.is_ok(),
7128 "get_update_editor4 failed: {:?}",
7129 result.err()
7130 );
7131 }
7132
7133 #[test]
7134 fn test_get_switch_editor_with_callbacks() {
7135 let tmp_dir = tempfile::tempdir().unwrap();
7137 let repos_path = tmp_dir.path().join("repo");
7138 let wc_path = tmp_dir.path().join("wc");
7139
7140 crate::repos::Repos::create(&repos_path).unwrap();
7142 let url_str = crate::path_to_file_url(&repos_path);
7143 let url = crate::uri::Uri::new(&url_str).unwrap();
7144
7145 let mut client_ctx = crate::client::Context::new().unwrap();
7147 client_ctx
7148 .checkout(
7149 url.clone(),
7150 &wc_path,
7151 &crate::client::CheckoutOptions {
7152 peg_revision: crate::Revision::Head,
7153 revision: crate::Revision::Head,
7154 depth: crate::Depth::Infinity,
7155 ignore_externals: false,
7156 allow_unver_obstructions: false,
7157 },
7158 )
7159 .unwrap();
7160
7161 let notify_called = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
7163 let notify_called_clone = notify_called.clone();
7164
7165 let mut wc_ctx = Context::new().unwrap();
7166 let options = SwitchEditorOptions {
7167 notify_func: Some(Box::new(move |_notify| {
7168 notify_called_clone.store(true, std::sync::atomic::Ordering::SeqCst);
7169 })),
7170 ..Default::default()
7171 };
7172 let result =
7173 wc_ctx.get_switch_editor(wc_path.to_str().unwrap(), "", url_str.as_str(), options);
7174
7175 assert!(
7176 result.is_ok(),
7177 "get_switch_editor failed: {:?}",
7178 result.err()
7179 );
7180 }
7181
7182 #[test]
7183 fn test_get_update_editor4_server_performs_filtering() {
7184 let temp_dir = tempfile::tempdir().unwrap();
7186 std::fs::create_dir_all(temp_dir.path()).unwrap();
7187
7188 let mut wc_ctx = Context::new().unwrap();
7189
7190 let options = UpdateEditorOptions {
7192 server_performs_filtering: true,
7193 ..Default::default()
7194 };
7195 let result =
7196 get_update_editor4(&mut wc_ctx, temp_dir.path().to_str().unwrap(), "", options);
7197
7198 assert!(result.is_err());
7200 }
7201
7202 #[test]
7203 fn test_get_update_editor4_clean_checkout() {
7204 let temp_dir = tempfile::tempdir().unwrap();
7206 std::fs::create_dir_all(temp_dir.path()).unwrap();
7207
7208 let mut wc_ctx = Context::new().unwrap();
7209
7210 let options = UpdateEditorOptions {
7212 clean_checkout: true,
7213 ..Default::default()
7214 };
7215 let result =
7216 get_update_editor4(&mut wc_ctx, temp_dir.path().to_str().unwrap(), "", options);
7217
7218 assert!(result.is_err());
7220 }
7221
7222 #[test]
7223 fn test_get_switch_editor_server_performs_filtering() {
7224 let temp_dir = tempfile::tempdir().unwrap();
7226 std::fs::create_dir_all(temp_dir.path()).unwrap();
7227
7228 let mut wc_ctx = Context::new().unwrap();
7229
7230 let options = SwitchEditorOptions {
7232 server_performs_filtering: true,
7233 ..Default::default()
7234 };
7235 let result = wc_ctx.get_switch_editor(
7236 temp_dir.path().to_str().unwrap(),
7237 "",
7238 "http://example.com/svn/trunk",
7239 options,
7240 );
7241
7242 assert!(result.is_err());
7244 }
7245
7246 #[test]
7247 fn test_parse_externals_description() {
7248 let desc = "^/trunk/lib lib";
7250 let items = parse_externals_description("/parent", desc, true).unwrap();
7251 assert_eq!(items.len(), 1);
7252 assert_eq!(items[0].target_dir, "lib");
7253 assert!(items[0].url.contains("trunk/lib"));
7254 }
7255
7256 #[test]
7257 fn test_parse_externals_description_with_revision() {
7258 let desc = "-r42 http://example.com/svn/trunk external_dir";
7260 let items = parse_externals_description("/parent", desc, false).unwrap();
7261 assert_eq!(items.len(), 1);
7262 assert_eq!(items[0].target_dir, "external_dir");
7263 assert_eq!(items[0].url, "http://example.com/svn/trunk");
7264 }
7265
7266 #[test]
7267 fn test_parse_externals_description_empty() {
7268 let items = parse_externals_description("/parent", "", true).unwrap();
7270 assert!(items.is_empty());
7271 }
7272
7273 #[test]
7274 fn test_wc_add_function() {
7275 use tempfile::TempDir;
7276
7277 let temp_dir = TempDir::new().unwrap();
7278 let repos_path = temp_dir.path().join("repos");
7279 let wc_path = temp_dir.path().join("wc");
7280
7281 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7283
7284 let url_str = crate::path_to_file_url(&repos_path);
7286 let url = crate::uri::Uri::new(&url_str).unwrap();
7287 let mut client_ctx = crate::client::Context::new().unwrap();
7288 client_ctx
7289 .checkout(
7290 url,
7291 &wc_path,
7292 &crate::client::CheckoutOptions {
7293 peg_revision: crate::Revision::Head,
7294 revision: crate::Revision::Head,
7295 depth: crate::Depth::Infinity,
7296 ignore_externals: false,
7297 allow_unver_obstructions: false,
7298 },
7299 )
7300 .unwrap();
7301
7302 let file_path = wc_path.join("new_file.txt");
7304 std::fs::write(&file_path, "test content").unwrap();
7305
7306 let mut wc_ctx = Context::new().unwrap();
7309 let result = add(
7310 &mut wc_ctx,
7311 &file_path,
7312 crate::Depth::Infinity,
7313 false, false, false, false, );
7318
7319 assert!(result.is_err(), "add() should fail without write lock");
7321 let err_msg = format!("{:?}", result.unwrap_err());
7322 assert!(
7323 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
7324 "Expected lock-related error, got: {}",
7325 err_msg
7326 );
7327 }
7328
7329 #[test]
7330 fn test_wc_delete_keep_local() {
7331 use tempfile::TempDir;
7332
7333 let temp_dir = TempDir::new().unwrap();
7334 let repos_path = temp_dir.path().join("repos");
7335 let wc_path = temp_dir.path().join("wc");
7336
7337 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7339
7340 let url_str = crate::path_to_file_url(&repos_path);
7342 let url = crate::uri::Uri::new(&url_str).unwrap();
7343 let mut client_ctx = crate::client::Context::new().unwrap();
7344 client_ctx
7345 .checkout(
7346 url,
7347 &wc_path,
7348 &crate::client::CheckoutOptions {
7349 peg_revision: crate::Revision::Head,
7350 revision: crate::Revision::Head,
7351 depth: crate::Depth::Infinity,
7352 ignore_externals: false,
7353 allow_unver_obstructions: false,
7354 },
7355 )
7356 .unwrap();
7357
7358 let file_path = wc_path.join("to_delete.txt");
7360 std::fs::write(&file_path, "test content").unwrap();
7361 client_ctx
7362 .add(&file_path, &crate::client::AddOptions::new())
7363 .unwrap();
7364
7365 let mut committed = false;
7367 client_ctx
7368 .commit(
7369 &[wc_path.to_str().unwrap()],
7370 &crate::client::CommitOptions::default(),
7371 std::collections::HashMap::from([("svn:log", "Add test file")]),
7372 None,
7373 &mut |_info| {
7374 committed = true;
7375 Ok(())
7376 },
7377 )
7378 .unwrap();
7379 assert!(committed);
7380
7381 assert!(file_path.exists());
7383
7384 let mut wc_ctx = Context::new().unwrap();
7387 let result = delete(
7388 &mut wc_ctx,
7389 &file_path,
7390 true, false, );
7393
7394 assert!(result.is_err(), "delete() should fail without write lock");
7396 let err_msg = format!("{:?}", result.unwrap_err());
7397 assert!(
7398 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
7399 "Expected lock-related error, got: {}",
7400 err_msg
7401 );
7402
7403 assert!(file_path.exists());
7405 }
7406
7407 #[test]
7408 fn test_wc_delete_remove_local() {
7409 use tempfile::TempDir;
7410
7411 let temp_dir = TempDir::new().unwrap();
7412 let repos_path = temp_dir.path().join("repos");
7413 let wc_path = temp_dir.path().join("wc");
7414
7415 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7417
7418 let url_str = crate::path_to_file_url(&repos_path);
7420 let url = crate::uri::Uri::new(&url_str).unwrap();
7421 let mut client_ctx = crate::client::Context::new().unwrap();
7422 client_ctx
7423 .checkout(
7424 url,
7425 &wc_path,
7426 &crate::client::CheckoutOptions {
7427 peg_revision: crate::Revision::Head,
7428 revision: crate::Revision::Head,
7429 depth: crate::Depth::Infinity,
7430 ignore_externals: false,
7431 allow_unver_obstructions: false,
7432 },
7433 )
7434 .unwrap();
7435
7436 let file_path = wc_path.join("to_remove.txt");
7438 std::fs::write(&file_path, "test content").unwrap();
7439 client_ctx
7440 .add(&file_path, &crate::client::AddOptions::new())
7441 .unwrap();
7442
7443 let mut committed = false;
7445 client_ctx
7446 .commit(
7447 &[wc_path.to_str().unwrap()],
7448 &crate::client::CommitOptions::default(),
7449 std::collections::HashMap::from([("svn:log", "Add test file")]),
7450 None,
7451 &mut |_info| {
7452 committed = true;
7453 Ok(())
7454 },
7455 )
7456 .unwrap();
7457 assert!(committed);
7458 assert!(file_path.exists());
7459
7460 let mut wc_ctx = Context::new().unwrap();
7463 let result = delete(
7464 &mut wc_ctx,
7465 &file_path,
7466 false, false, );
7469
7470 assert!(result.is_err(), "delete() should fail without write lock");
7472 let err_msg = format!("{:?}", result.unwrap_err());
7473 assert!(
7474 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
7475 "Expected lock-related error, got: {}",
7476 err_msg
7477 );
7478
7479 assert!(file_path.exists());
7481 }
7482
7483 #[test]
7484 fn test_revision_status_empty_wc() {
7485 use tempfile::TempDir;
7486
7487 let temp_dir = TempDir::new().unwrap();
7488 let repos_path = temp_dir.path().join("repos");
7489 let wc_path = temp_dir.path().join("wc");
7490
7491 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7493
7494 let url_str = crate::path_to_file_url(&repos_path);
7496 let url = crate::uri::Uri::new(&url_str).unwrap();
7497 let mut client_ctx = crate::client::Context::new().unwrap();
7498 client_ctx
7499 .checkout(
7500 url,
7501 &wc_path,
7502 &crate::client::CheckoutOptions {
7503 peg_revision: crate::Revision::Head,
7504 revision: crate::Revision::Head,
7505 depth: crate::Depth::Infinity,
7506 ignore_externals: false,
7507 allow_unver_obstructions: false,
7508 },
7509 )
7510 .unwrap();
7511
7512 let result = revision_status(&wc_path, None, false);
7514 assert!(result.is_ok(), "revision_status should succeed on empty WC");
7515
7516 let (min_rev, max_rev, is_switched, is_modified) = result.unwrap();
7517
7518 assert_eq!(min_rev, 0, "min_rev should be 0 for empty WC");
7520 assert_eq!(max_rev, 0, "max_rev should be 0 for empty WC");
7521 assert_eq!(is_switched, false, "should not be switched");
7522 assert_eq!(is_modified, false, "should not be modified");
7523 }
7524
7525 #[test]
7526 fn test_revision_status_with_modifications() {
7527 use tempfile::TempDir;
7528
7529 let temp_dir = TempDir::new().unwrap();
7530 let repos_path = temp_dir.path().join("repos");
7531 let wc_path = temp_dir.path().join("wc");
7532
7533 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7535
7536 let url_str = crate::path_to_file_url(&repos_path);
7538 let url = crate::uri::Uri::new(&url_str).unwrap();
7539 let mut client_ctx = crate::client::Context::new().unwrap();
7540 client_ctx
7541 .checkout(
7542 url,
7543 &wc_path,
7544 &crate::client::CheckoutOptions {
7545 peg_revision: crate::Revision::Head,
7546 revision: crate::Revision::Head,
7547 depth: crate::Depth::Infinity,
7548 ignore_externals: false,
7549 allow_unver_obstructions: false,
7550 },
7551 )
7552 .unwrap();
7553
7554 let file_path = wc_path.join("test.txt");
7556 std::fs::write(&file_path, "test content").unwrap();
7557 client_ctx
7558 .add(&file_path, &crate::client::AddOptions::new())
7559 .unwrap();
7560
7561 let result = revision_status(&wc_path, None, false);
7563 assert!(
7564 result.is_ok(),
7565 "revision_status should succeed with modifications"
7566 );
7567
7568 let (min_rev, max_rev, is_switched, is_modified) = result.unwrap();
7569
7570 assert_eq!(min_rev, 0, "min_rev should be 0");
7572 assert_eq!(max_rev, 0, "max_rev should be 0");
7573 assert_eq!(is_switched, false, "should not be switched");
7574 assert_eq!(is_modified, true, "should be modified after adding file");
7575 }
7576
7577 #[test]
7578 fn test_revision_status_after_commit() {
7579 use tempfile::TempDir;
7580
7581 let temp_dir = TempDir::new().unwrap();
7582 let repos_path = temp_dir.path().join("repos");
7583 let wc_path = temp_dir.path().join("wc");
7584
7585 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7587
7588 let url_str = crate::path_to_file_url(&repos_path);
7590 let url = crate::uri::Uri::new(&url_str).unwrap();
7591 let mut client_ctx = crate::client::Context::new().unwrap();
7592 client_ctx
7593 .checkout(
7594 url,
7595 &wc_path,
7596 &crate::client::CheckoutOptions {
7597 peg_revision: crate::Revision::Head,
7598 revision: crate::Revision::Head,
7599 depth: crate::Depth::Infinity,
7600 ignore_externals: false,
7601 allow_unver_obstructions: false,
7602 },
7603 )
7604 .unwrap();
7605
7606 let file_path = wc_path.join("test.txt");
7608 std::fs::write(&file_path, "test content").unwrap();
7609 client_ctx
7610 .add(&file_path, &crate::client::AddOptions::new())
7611 .unwrap();
7612
7613 let mut committed = false;
7614 client_ctx
7615 .commit(
7616 &[wc_path.to_str().unwrap()],
7617 &crate::client::CommitOptions::default(),
7618 std::collections::HashMap::from([("svn:log", "Add test file")]),
7619 None,
7620 &mut |_info| {
7621 committed = true;
7622 Ok(())
7623 },
7624 )
7625 .unwrap();
7626 assert!(committed, "commit should have been called");
7627
7628 let result = revision_status(&wc_path, None, false);
7630 assert!(
7631 result.is_ok(),
7632 "revision_status should succeed after commit"
7633 );
7634
7635 let (min_rev, max_rev, is_switched, is_modified) = result.unwrap();
7636
7637 assert!(
7640 max_rev >= 1,
7641 "max_rev should be at least 1 after commit, got {}",
7642 max_rev
7643 );
7644 assert!(
7645 min_rev <= max_rev,
7646 "min_rev ({}) should be <= max_rev ({})",
7647 min_rev,
7648 max_rev
7649 );
7650 assert_eq!(is_switched, false, "should not be switched");
7651 assert_eq!(is_modified, false, "should not be modified after commit");
7652 }
7653
7654 #[test]
7655 fn test_resolve_conflict_function() {
7656 use tempfile::TempDir;
7657
7658 let temp_dir = TempDir::new().unwrap();
7659
7660 let non_wc_path = temp_dir.path().join("not_a_wc");
7663 std::fs::create_dir(&non_wc_path).unwrap();
7664
7665 let mut wc_ctx = Context::new().unwrap();
7666 let result = resolve_conflict(
7667 &mut wc_ctx,
7668 &non_wc_path,
7669 crate::Depth::Empty,
7670 true, true, false, ConflictChoice::Postpone,
7674 );
7675
7676 assert!(
7679 result.is_err(),
7680 "resolve_conflict() should fail on non-WC path, proving it calls the C library"
7681 );
7682 }
7683
7684 #[test]
7685 fn test_status_methods() {
7686 use tempfile::TempDir;
7687
7688 let temp_dir = TempDir::new().unwrap();
7689 let repos_path = temp_dir.path().join("repos");
7690 let wc_path = temp_dir.path().join("wc");
7691
7692 let _repos = crate::repos::Repos::create(&repos_path).unwrap();
7694
7695 let url_str = crate::path_to_file_url(&repos_path);
7697 let url = crate::uri::Uri::new(&url_str).unwrap();
7698 let mut client_ctx = crate::client::Context::new().unwrap();
7699 client_ctx
7700 .checkout(
7701 url,
7702 &wc_path,
7703 &crate::client::CheckoutOptions {
7704 peg_revision: crate::Revision::Head,
7705 revision: crate::Revision::Head,
7706 depth: crate::Depth::Infinity,
7707 ignore_externals: false,
7708 allow_unver_obstructions: false,
7709 },
7710 )
7711 .unwrap();
7712
7713 let file_path = wc_path.join("test.txt");
7715 std::fs::write(&file_path, "test content").unwrap();
7716 client_ctx
7717 .add(&file_path, &crate::client::AddOptions::new())
7718 .unwrap();
7719
7720 let mut committed = false;
7721 client_ctx
7722 .commit(
7723 &[wc_path.to_str().unwrap()],
7724 &crate::client::CommitOptions::default(),
7725 std::collections::HashMap::from([("svn:log", "Add test file")]),
7726 None,
7727 &mut |_info| {
7728 committed = true;
7729 Ok(())
7730 },
7731 )
7732 .unwrap();
7733 assert!(committed);
7734
7735 let mut wc_ctx = Context::new().unwrap();
7737 let mut found_status = false;
7738 wc_ctx
7739 .walk_status(
7740 &file_path,
7741 crate::Depth::Empty,
7742 true, false, false, None, |_path, status| {
7747 found_status = true;
7748
7749 assert_eq!(status.copied(), false, "File should not be copied");
7751 assert_eq!(status.switched(), false, "File should not be switched");
7752 assert_eq!(status.locked(), false, "File should not be locked");
7753 assert_eq!(
7754 status.node_status(),
7755 StatusKind::Normal,
7756 "File should be normal"
7757 );
7758 assert!(status.revision().0 >= 0, "Revision should be >= 0");
7759 assert_eq!(
7760 status.repos_relpath(),
7761 Some("test.txt".to_string()),
7762 "repos_relpath should match"
7763 );
7764 Ok(())
7765 },
7766 )
7767 .unwrap();
7768
7769 assert!(found_status, "Should have found file status");
7770 }
7771
7772 #[test]
7773 fn test_status_added_file() {
7774 let mut fixture = SvnTestFixture::new();
7775 let file_path = fixture.add_file("added.txt", "test content");
7776
7777 let mut wc_ctx = Context::new().unwrap();
7779 let mut found_status = false;
7780 wc_ctx
7781 .walk_status(
7782 &file_path,
7783 crate::Depth::Empty,
7784 true, false, false, None, |_path, status| {
7789 found_status = true;
7790
7791 assert_eq!(
7793 status.node_status(),
7794 StatusKind::Added,
7795 "File should be added"
7796 );
7797 assert_eq!(
7798 status.copied(),
7799 false,
7800 "Added file should not be marked as copied"
7801 );
7802 Ok(())
7803 },
7804 )
7805 .unwrap();
7806
7807 assert!(found_status, "Should have found file status");
7808 }
7809
7810 #[test]
7811 fn test_cleanup_actually_executes() {
7812 use tempfile::TempDir;
7813
7814 let temp_dir = TempDir::new().unwrap();
7817 let non_existent = temp_dir.path().join("does_not_exist");
7818
7819 let result = cleanup(&non_existent, false, false, false, false, false);
7820 assert!(
7821 result.is_err(),
7822 "cleanup() should fail on non-existent path, proving it calls the C library"
7823 );
7824
7825 let fixture = SvnTestFixture::new();
7827
7828 let result = cleanup(&fixture.wc_path, false, false, false, false, false);
7830 assert!(
7831 result.is_ok(),
7832 "cleanup() should succeed on valid working copy"
7833 );
7834
7835 let result = cleanup(&fixture.wc_path, true, true, true, true, false);
7837 assert!(
7838 result.is_ok(),
7839 "cleanup() with all options should succeed on valid working copy"
7840 );
7841 }
7842
7843 #[test]
7844 fn test_context_check_wc_returns_format_number() {
7845 let fixture = SvnTestFixture::new();
7846
7847 let mut wc_ctx = Context::new().unwrap();
7849 let format_num = wc_ctx.check_wc(fixture.wc_path_str()).unwrap();
7850
7851 assert!(
7855 format_num > 10,
7856 "Working copy format should be > 10 for modern SVN, got {}",
7857 format_num
7858 );
7859 assert!(
7860 format_num < 100,
7861 "Working copy format should be reasonable (< 100), got {}",
7862 format_num
7863 );
7864 }
7865
7866 #[test]
7867 fn test_free_function_check_wc() {
7868 use tempfile::TempDir;
7869
7870 let temp_dir = TempDir::new().unwrap();
7871
7872 let non_wc = temp_dir.path().join("not_a_wc");
7874 std::fs::create_dir(&non_wc).unwrap();
7875
7876 let result = check_wc(&non_wc);
7877 assert!(
7878 result.is_ok(),
7879 "check_wc should succeed on non-WC directory"
7880 );
7881 assert_eq!(
7882 result.unwrap(),
7883 None,
7884 "check_wc should return None for non-WC directory"
7885 );
7886
7887 let fixture = SvnTestFixture::new();
7889 let result = check_wc(&fixture.wc_path);
7890 assert!(result.is_ok(), "check_wc should succeed on valid WC");
7891
7892 let format_opt = result.unwrap();
7893 assert!(
7894 format_opt.is_some(),
7895 "check_wc should return Some for valid WC, got None"
7896 );
7897
7898 let format_num = format_opt.unwrap();
7899 assert!(
7900 format_num > 10,
7901 "WC format should be > 10 for modern SVN, got {}",
7902 format_num
7903 );
7904 assert!(
7905 format_num < 100,
7906 "WC format should be reasonable (< 100), got {}",
7907 format_num
7908 );
7909 }
7910
7911 #[test]
7912 fn test_upgrade_actually_executes() {
7913 use tempfile::TempDir;
7914
7915 let temp_dir = TempDir::new().unwrap();
7916 let non_wc = temp_dir.path().join("not_a_wc");
7917 std::fs::create_dir(&non_wc).unwrap();
7918
7919 let mut wc_ctx = Context::new().unwrap();
7920 let result = wc_ctx.upgrade(non_wc.to_str().unwrap());
7921
7922 assert!(
7924 result.is_err(),
7925 "upgrade() should fail on non-WC directory, proving it calls the C library"
7926 );
7927 }
7928
7929 #[test]
7930 fn test_prop_set_actually_executes() {
7931 let mut fixture = SvnTestFixture::new();
7932 let file_path = fixture.add_file("test.txt", "test content");
7933
7934 let mut wc_ctx = Context::new().unwrap();
7935
7936 assert_eq!(
7938 wc_ctx.prop_get(&file_path, "test:prop").unwrap(),
7939 None,
7940 "Property should not exist before"
7941 );
7942
7943 fixture
7945 .client_ctx
7946 .propset(
7947 "test:prop",
7948 Some(b"test value"),
7949 file_path.to_str().expect("test path should be valid UTF-8"),
7950 &crate::client::PropSetOptions::default(),
7951 )
7952 .unwrap();
7953
7954 assert_eq!(
7956 wc_ctx.prop_get(&file_path, "test:prop").unwrap().as_deref(),
7957 Some(&b"test value"[..]),
7958 "client propset() should actually set the property"
7959 );
7960 }
7961
7962 #[test]
7963 fn test_relocate_actually_executes() {
7964 let mut fixture = SvnTestFixture::new();
7965 let old_url = fixture.url.clone();
7966
7967 assert_eq!(fixture.get_wc_url(), old_url);
7969
7970 let (_repos_path2, new_url) = create_repo(fixture.temp_dir.path(), "repos2");
7972
7973 let mut wc_ctx = Context::new().unwrap();
7975 wc_ctx
7976 .relocate(fixture.wc_path_str(), &old_url, &new_url)
7977 .unwrap();
7978
7979 assert_eq!(
7981 fixture.get_wc_url(),
7982 new_url,
7983 "relocate() should actually change the repository URL"
7984 );
7985 }
7986
7987 #[test]
7988 fn test_add_lock_actually_executes() {
7989 use tempfile::TempDir;
7990
7991 let temp_dir = TempDir::new().unwrap();
7992 let non_wc = temp_dir.path().join("not_a_wc");
7993 std::fs::create_dir(&non_wc).unwrap();
7994 let file_path = non_wc.join("file.txt");
7995 std::fs::write(&file_path, "test").unwrap();
7996
7997 let mut wc_ctx = Context::new().unwrap();
7998
7999 let lock = Lock::from_ptr(std::ptr::null());
8001
8002 let result = wc_ctx.add_lock(&file_path, &lock);
8003
8004 assert!(
8006 result.is_err(),
8007 "add_lock() should fail on non-WC file, proving it calls the C library"
8008 );
8009 }
8010
8011 #[test]
8012 fn test_remove_lock_actually_executes() {
8013 use tempfile::TempDir;
8014
8015 let temp_dir = TempDir::new().unwrap();
8016 let non_wc = temp_dir.path().join("not_a_wc");
8017 std::fs::create_dir(&non_wc).unwrap();
8018 let file_path = non_wc.join("file.txt");
8019 std::fs::write(&file_path, "test").unwrap();
8020
8021 let mut wc_ctx = Context::new().unwrap();
8022 let result = wc_ctx.remove_lock(&file_path);
8023
8024 assert!(
8026 result.is_err(),
8027 "remove_lock() should fail on non-WC file, proving it calls the C library"
8028 );
8029 }
8030
8031 #[test]
8032 fn test_ensure_adm_actually_executes() {
8033 use tempfile::TempDir;
8034
8035 let temp_dir = TempDir::new().unwrap();
8036 let wc_path = temp_dir.path().join("new_wc");
8037
8038 std::fs::create_dir(&wc_path).unwrap();
8040
8041 let svn_dir = wc_path.join(".svn");
8043 assert!(!svn_dir.exists(), ".svn should not exist before ensure_adm");
8044
8045 let result = ensure_adm(
8046 &wc_path,
8047 "test-uuid",
8048 "file:///tmp/test-repo",
8049 "file:///tmp/test-repo",
8050 1,
8051 );
8052
8053 assert!(result.is_ok(), "ensure_adm() should succeed: {:?}", result);
8055
8056 assert!(
8058 svn_dir.exists(),
8059 "ensure_adm() should create .svn directory"
8060 );
8061 }
8062
8063 #[test]
8064 fn test_process_committed_queue_actually_executes() {
8065 use tempfile::TempDir;
8066
8067 let temp_dir = TempDir::new().unwrap();
8068 let non_wc = temp_dir.path().join("not_a_wc");
8069 std::fs::create_dir(&non_wc).unwrap();
8070
8071 let mut wc_ctx = Context::new().unwrap();
8072 let mut queue = CommittedQueue::new();
8073
8074 let result = wc_ctx.process_committed_queue(
8075 &mut queue,
8076 crate::Revnum(1),
8077 Some("2024-01-01T00:00:00.000000Z"),
8078 Some("author"),
8079 );
8080
8081 assert!(
8085 result.is_ok(),
8086 "process_committed_queue() should succeed on empty queue"
8087 );
8088
8089 }
8094
8095 struct RecordingDiffCallbacks {
8098 changed_files: Vec<String>,
8099 added_files: Vec<String>,
8100 deleted_files: Vec<String>,
8101 }
8102
8103 impl RecordingDiffCallbacks {
8104 fn new() -> Self {
8105 Self {
8106 changed_files: Vec::new(),
8107 added_files: Vec::new(),
8108 deleted_files: Vec::new(),
8109 }
8110 }
8111 }
8112
8113 impl DiffCallbacks for RecordingDiffCallbacks {
8114 fn file_opened(
8115 &mut self,
8116 _path: &str,
8117 _rev: crate::Revnum,
8118 ) -> Result<(bool, bool), crate::Error<'static>> {
8119 Ok((false, false))
8120 }
8121
8122 fn file_changed(
8123 &mut self,
8124 change: &FileChange<'_>,
8125 ) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>> {
8126 self.changed_files.push(change.path.to_string());
8127 Ok((NotifyState::Changed, NotifyState::Unchanged, false))
8128 }
8129
8130 fn file_added(
8131 &mut self,
8132 change: &FileChange<'_>,
8133 _copyfrom_path: Option<&str>,
8134 _copyfrom_revision: crate::Revnum,
8135 ) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>> {
8136 self.added_files.push(change.path.to_string());
8137 Ok((NotifyState::Changed, NotifyState::Unchanged, false))
8138 }
8139
8140 fn file_deleted(
8141 &mut self,
8142 path: &str,
8143 _tmpfile1: Option<&str>,
8144 _tmpfile2: Option<&str>,
8145 _mimetype1: Option<&str>,
8146 _mimetype2: Option<&str>,
8147 ) -> Result<(NotifyState, bool), crate::Error<'static>> {
8148 self.deleted_files.push(path.to_string());
8149 Ok((NotifyState::Changed, false))
8150 }
8151
8152 fn dir_deleted(
8153 &mut self,
8154 _path: &str,
8155 ) -> Result<(NotifyState, bool), crate::Error<'static>> {
8156 Ok((NotifyState::Unchanged, false))
8157 }
8158
8159 fn dir_opened(
8160 &mut self,
8161 _path: &str,
8162 _rev: crate::Revnum,
8163 ) -> Result<(bool, bool, bool), crate::Error<'static>> {
8164 Ok((false, false, false))
8165 }
8166
8167 fn dir_added(
8168 &mut self,
8169 _path: &str,
8170 _rev: crate::Revnum,
8171 _copyfrom_path: Option<&str>,
8172 _copyfrom_revision: crate::Revnum,
8173 ) -> Result<(NotifyState, bool, bool, bool), crate::Error<'static>> {
8174 Ok((NotifyState::Changed, false, false, false))
8175 }
8176
8177 fn dir_props_changed(
8178 &mut self,
8179 _path: &str,
8180 _dir_was_added: bool,
8181 _prop_changes: &[PropChange],
8182 ) -> Result<(NotifyState, bool), crate::Error<'static>> {
8183 Ok((NotifyState::Unchanged, false))
8184 }
8185
8186 fn dir_closed(
8187 &mut self,
8188 _path: &str,
8189 _dir_was_added: bool,
8190 ) -> Result<(NotifyState, NotifyState, bool), crate::Error<'static>> {
8191 Ok((NotifyState::Unchanged, NotifyState::Unchanged, false))
8192 }
8193 }
8194
8195 #[test]
8196 fn test_diff_reports_modified_file() {
8197 let mut fixture = SvnTestFixture::new();
8198 fixture.add_file("test.txt", "original content\n");
8199 fixture.commit();
8200
8201 std::fs::write(fixture.wc_path.join("test.txt"), "modified content\n").unwrap();
8203
8204 let mut callbacks = RecordingDiffCallbacks::new();
8205 let mut wc_ctx = Context::new().unwrap();
8206 wc_ctx
8207 .diff(&fixture.wc_path, &DiffOptions::default(), &mut callbacks)
8208 .expect("diff() should succeed on a working copy with local modifications");
8209
8210 assert_eq!(
8212 callbacks.changed_files,
8213 vec!["test.txt"],
8214 "only the modified file should be reported as changed"
8215 );
8216 assert!(
8217 callbacks.added_files.is_empty(),
8218 "no files should be reported as added"
8219 );
8220 assert!(
8221 callbacks.deleted_files.is_empty(),
8222 "no files should be reported as deleted"
8223 );
8224 }
8225
8226 #[test]
8227 fn test_diff_reports_added_file() {
8228 let mut fixture = SvnTestFixture::new();
8229 fixture.add_file("existing.txt", "exists\n");
8231 fixture.commit();
8232
8233 fixture.add_file("new.txt", "brand new\n");
8235
8236 let mut callbacks = RecordingDiffCallbacks::new();
8237 let mut wc_ctx = Context::new().unwrap();
8238 wc_ctx
8239 .diff(&fixture.wc_path, &DiffOptions::default(), &mut callbacks)
8240 .expect("diff() should succeed");
8241
8242 assert_eq!(
8244 callbacks.added_files,
8245 vec!["new.txt"],
8246 "the newly added file should be reported as added"
8247 );
8248 assert!(
8249 callbacks.changed_files.is_empty(),
8250 "no files should be reported as changed"
8251 );
8252 }
8253
8254 #[test]
8255 fn test_diff_clean_working_copy_reports_nothing() {
8256 let mut fixture = SvnTestFixture::new();
8257 fixture.add_file("clean.txt", "clean content\n");
8258 fixture.commit();
8259
8260 let mut callbacks = RecordingDiffCallbacks::new();
8261 let mut wc_ctx = Context::new().unwrap();
8262 wc_ctx
8263 .diff(&fixture.wc_path, &DiffOptions::default(), &mut callbacks)
8264 .expect("diff() should succeed on a clean working copy");
8265
8266 assert!(
8267 callbacks.changed_files.is_empty(),
8268 "no changed files in a clean WC"
8269 );
8270 assert!(
8271 callbacks.added_files.is_empty(),
8272 "no added files in a clean WC"
8273 );
8274 assert!(
8275 callbacks.deleted_files.is_empty(),
8276 "no deleted files in a clean WC"
8277 );
8278 }
8279
8280 #[test]
8281 fn test_merge_requires_write_lock() {
8282 let mut fixture = SvnTestFixture::new();
8288 let target_path = fixture.add_file("target.txt", "line 1\nline 2\nline 3\n");
8289 fixture.commit();
8290
8291 let left_path = fixture.temp_dir.path().join("left.txt");
8292 let right_path = fixture.temp_dir.path().join("right.txt");
8293 std::fs::write(&left_path, "line 1\nline 2\nline 3\n").unwrap();
8294 std::fs::write(&right_path, "line 1\nline 2 modified\nline 3\n").unwrap();
8295
8296 let mut wc_ctx = Context::new().unwrap();
8297 let result = wc_ctx.merge(
8298 &left_path,
8299 &right_path,
8300 &target_path,
8301 Some(".left"),
8302 Some(".right"),
8303 Some(".working"),
8304 &[],
8305 &MergeOptions::default(),
8306 );
8307
8308 assert!(
8311 result.is_err(),
8312 "merge() must fail without a write lock; a mutation returning Ok(()) would be caught here"
8313 );
8314 let err_msg = format!("{:?}", result.unwrap_err());
8315 assert!(
8316 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
8317 "expected a write-lock error, got: {}",
8318 err_msg
8319 );
8320 }
8321
8322 #[test]
8323 fn test_merge_with_prop_diff_requires_write_lock() {
8324 let mut fixture = SvnTestFixture::new();
8327 let target_path = fixture.add_file("target.txt", "content\n");
8328 fixture.commit();
8329
8330 let left_path = fixture.temp_dir.path().join("left.txt");
8331 let right_path = fixture.temp_dir.path().join("right.txt");
8332 std::fs::write(&left_path, "content\n").unwrap();
8333 std::fs::write(&right_path, "content modified\n").unwrap();
8334
8335 let prop_changes = vec![PropChange {
8336 name: "svn:eol-style".to_string(),
8337 value: Some(b"native".to_vec()),
8338 }];
8339
8340 let mut wc_ctx = Context::new().unwrap();
8341 let result = wc_ctx.merge(
8342 &left_path,
8343 &right_path,
8344 &target_path,
8345 None,
8346 None,
8347 None,
8348 &prop_changes,
8349 &MergeOptions::default(),
8350 );
8351
8352 assert!(
8353 result.is_err(),
8354 "merge() with prop_diff must fail without a write lock"
8355 );
8356 let err_msg = format!("{:?}", result.unwrap_err());
8357 assert!(
8358 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
8359 "expected a write-lock error, got: {}",
8360 err_msg
8361 );
8362 }
8363
8364 #[test]
8365 fn test_merge_props_requires_write_lock() {
8366 let mut fixture = SvnTestFixture::new();
8370 let file_path = fixture.add_file("propmerge.txt", "content\n");
8371 fixture.commit();
8372
8373 let prop_changes = vec![PropChange {
8374 name: "svn:keywords".to_string(),
8375 value: Some(b"Id".to_vec()),
8376 }];
8377
8378 let mut wc_ctx = Context::new().unwrap();
8379 let result = wc_ctx.merge_props(&file_path, None, &prop_changes, false, None, None);
8380
8381 assert!(
8382 result.is_err(),
8383 "merge_props() must fail without a write lock; a mutation returning Ok(()) would be caught here"
8384 );
8385 let err_msg = format!("{:?}", result.unwrap_err());
8386 assert!(
8387 err_msg.to_lowercase().contains("lock")
8388 || err_msg.to_lowercase().contains("write")
8389 || err_msg.to_lowercase().contains("path"),
8390 "expected a write-lock or path error, got: {}",
8391 err_msg
8392 );
8393 }
8394
8395 #[test]
8396 fn test_revert_requires_write_lock() {
8397 let mut fixture = SvnTestFixture::new();
8402 let file_path = fixture.add_file("revert_me.txt", "original\n");
8403 fixture.commit();
8404
8405 std::fs::write(&file_path, "changed\n").unwrap();
8406
8407 let mut wc_ctx = Context::new().unwrap();
8408 let result = wc_ctx.revert(
8409 &file_path,
8410 &RevertOptions {
8411 depth: crate::Depth::Empty,
8412 ..Default::default()
8413 },
8414 );
8415
8416 assert!(
8417 result.is_err(),
8418 "revert() must fail without a write lock; a mutation returning Ok(()) would be caught here"
8419 );
8420 let err_msg = format!("{:?}", result.unwrap_err());
8421 assert!(
8422 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
8423 "expected a write-lock error, got: {}",
8424 err_msg
8425 );
8426 }
8427
8428 #[test]
8429 fn test_revert_with_added_keep_local_false_requires_write_lock() {
8430 let mut fixture = SvnTestFixture::new();
8432 fixture.add_file("existing.txt", "exists\n");
8433 fixture.commit();
8434
8435 let new_file = fixture.add_file("new_file.txt", "new\n");
8436
8437 let mut wc_ctx = Context::new().unwrap();
8438 let result = wc_ctx.revert(
8439 &new_file,
8440 &RevertOptions {
8441 depth: crate::Depth::Empty,
8442 added_keep_local: false,
8443 ..Default::default()
8444 },
8445 );
8446
8447 assert!(
8448 result.is_err(),
8449 "revert() with added_keep_local=false must fail without a write lock"
8450 );
8451 let err_msg = format!("{:?}", result.unwrap_err());
8452 assert!(
8453 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
8454 "expected a write-lock error, got: {}",
8455 err_msg
8456 );
8457 }
8458
8459 #[test]
8460 fn test_revert_with_non_matching_changelist_is_noop() {
8461 let mut fixture = SvnTestFixture::new();
8466 let file_path = fixture.add_file("cl_file.txt", "content\n");
8467 fixture.commit();
8468
8469 std::fs::write(&file_path, "modified\n").unwrap();
8470
8471 let mut wc_ctx = Context::new().unwrap();
8472 wc_ctx
8475 .revert(
8476 &fixture.wc_path,
8477 &RevertOptions {
8478 depth: crate::Depth::Infinity,
8479 changelists: vec!["no-such-changelist".to_string()],
8480 ..Default::default()
8481 },
8482 )
8483 .expect("revert() with non-matching changelist should succeed as a no-op");
8484
8485 assert_eq!(
8487 std::fs::read_to_string(&file_path).unwrap(),
8488 "modified\n",
8489 "file should be unchanged when changelist filter matches nothing"
8490 );
8491 }
8492
8493 #[test]
8494 fn test_copy_requires_write_lock() {
8495 let mut fixture = SvnTestFixture::new();
8499 let src_path = fixture.add_file("src.txt", "source content\n");
8500 fixture.commit();
8501
8502 let dst_path = fixture.wc_path.join("dst.txt");
8503
8504 let mut wc_ctx = Context::new().unwrap();
8505 let result = wc_ctx.copy(&src_path, &dst_path, false);
8506
8507 assert!(
8508 result.is_err(),
8509 "copy() must fail without a write lock; a mutation returning Ok(()) would be caught here"
8510 );
8511 let err_msg = format!("{:?}", result.unwrap_err());
8512 assert!(
8513 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
8514 "expected a write-lock error, got: {}",
8515 err_msg
8516 );
8517 }
8518
8519 #[test]
8520 fn test_set_changelist_assigns_and_clears() {
8521 let mut fixture = SvnTestFixture::new();
8522 let file_path = fixture.add_file("hello.txt", "hello\n");
8523 fixture.commit();
8524
8525 let mut wc_ctx = Context::new().unwrap();
8526
8527 wc_ctx
8529 .set_changelist(&file_path, Some("my-cl"), crate::Depth::Empty, &[])
8530 .expect("set_changelist() should succeed for a versioned file");
8531
8532 let filter = vec!["my-cl".to_string()];
8536 let mut found: Vec<(String, Option<String>)> = Vec::new();
8537 wc_ctx
8538 .get_changelists(
8539 &fixture.wc_path,
8540 crate::Depth::Infinity,
8541 &filter,
8542 |path, cl| {
8543 found.push((path.to_owned(), cl.map(str::to_owned)));
8544 Ok(())
8545 },
8546 )
8547 .expect("get_changelists() should succeed");
8548
8549 assert_eq!(found.len(), 1, "expected exactly one changelist entry");
8550 assert_eq!(found[0].0, file_path.to_str().unwrap());
8551 assert_eq!(found[0].1, Some("my-cl".to_owned()));
8552
8553 wc_ctx
8555 .set_changelist(&file_path, None, crate::Depth::Empty, &[])
8556 .expect("clearing changelist should succeed");
8557
8558 let mut after: Vec<(String, Option<String>)> = Vec::new();
8560 wc_ctx
8561 .get_changelists(
8562 &fixture.wc_path,
8563 crate::Depth::Infinity,
8564 &filter,
8565 |path, cl| {
8566 after.push((path.to_owned(), cl.map(str::to_owned)));
8567 Ok(())
8568 },
8569 )
8570 .expect("get_changelists() after clear should succeed");
8571
8572 assert_eq!(after.len(), 0, "expected no changelist entries after clear");
8573 }
8574
8575 #[test]
8576 fn test_get_changelists_empty_wc() {
8577 let fixture = SvnTestFixture::new();
8581
8582 let mut wc_ctx = Context::new().unwrap();
8583 let mut with_changelist = 0usize;
8584 wc_ctx
8585 .get_changelists(
8586 &fixture.wc_path,
8587 crate::Depth::Infinity,
8588 &[],
8589 |_path, cl| {
8590 if cl.is_some() {
8591 with_changelist += 1;
8592 }
8593 Ok(())
8594 },
8595 )
8596 .expect("get_changelists() on clean WC should succeed");
8597
8598 assert_eq!(
8599 with_changelist, 0,
8600 "expected zero nodes with a changelist in a fresh WC"
8601 );
8602 }
8603
8604 #[test]
8605 fn test_get_changelists_filter() {
8606 let mut fixture = SvnTestFixture::new();
8607 let file_a = fixture.add_file("a.txt", "a\n");
8608 let file_b = fixture.add_file("b.txt", "b\n");
8609 fixture.commit();
8610
8611 let mut wc_ctx = Context::new().unwrap();
8612
8613 wc_ctx
8614 .set_changelist(&file_a, Some("cl-alpha"), crate::Depth::Empty, &[])
8615 .expect("set cl-alpha on a.txt");
8616 wc_ctx
8617 .set_changelist(&file_b, Some("cl-beta"), crate::Depth::Empty, &[])
8618 .expect("set cl-beta on b.txt");
8619
8620 let filter = vec!["cl-alpha".to_string()];
8624 let mut found: Vec<(String, Option<String>)> = Vec::new();
8625 wc_ctx
8626 .get_changelists(
8627 &fixture.wc_path,
8628 crate::Depth::Infinity,
8629 &filter,
8630 |path, cl| {
8631 found.push((path.to_owned(), cl.map(str::to_owned)));
8632 Ok(())
8633 },
8634 )
8635 .expect("get_changelists() with filter should succeed");
8636
8637 assert_eq!(found.len(), 1, "expected only the cl-alpha entry");
8638 assert_eq!(found[0].0, file_a.to_str().unwrap());
8639 assert_eq!(found[0].1, Some("cl-alpha".to_owned()));
8640 }
8641
8642 #[test]
8643 fn test_status_unmodified_file() {
8644 let mut fixture = SvnTestFixture::new();
8645 let file_path = fixture.add_file("hello.txt", "hello\n");
8646 fixture.commit();
8647
8648 let mut wc_ctx = Context::new().unwrap();
8649 let status = wc_ctx
8650 .status(&file_path)
8651 .expect("status() should succeed for a versioned file");
8652
8653 assert_eq!(status.node_status(), StatusKind::Normal);
8654 assert_eq!(status.text_status(), StatusKind::Normal);
8655 }
8656
8657 #[test]
8658 fn test_status_modified_file() {
8659 let mut fixture = SvnTestFixture::new();
8660 let file_path = fixture.add_file("hello.txt", "hello\n");
8661 fixture.commit();
8662
8663 std::fs::write(&file_path, "modified\n").unwrap();
8665
8666 let mut wc_ctx = Context::new().unwrap();
8667 let status = wc_ctx
8668 .status(&file_path)
8669 .expect("status() should succeed for a modified file");
8670
8671 assert_eq!(status.node_status(), StatusKind::Modified);
8672 }
8673
8674 #[test]
8675 fn test_status_dup() {
8676 let mut fixture = SvnTestFixture::new();
8677 let file_path = fixture.add_file("hello.txt", "hello\n");
8678 fixture.commit();
8679
8680 std::fs::write(&file_path, "modified\n").unwrap();
8682
8683 let mut wc_ctx = Context::new().unwrap();
8684 let status = wc_ctx.status(&file_path).expect("status() should succeed");
8685
8686 let duped = status.dup();
8687
8688 assert_eq!(duped.node_status(), status.node_status());
8689 assert_eq!(duped.text_status(), status.text_status());
8690 assert_eq!(duped.copied(), status.copied());
8691 assert_eq!(duped.switched(), status.switched());
8692 assert_eq!(duped.locked(), status.locked());
8693 assert_eq!(duped.versioned(), status.versioned());
8694 assert_eq!(duped.revision(), status.revision());
8695 assert_eq!(duped.changed_rev(), status.changed_rev());
8696 assert_eq!(duped.repos_relpath(), status.repos_relpath());
8697 assert_eq!(duped.repos_uuid(), status.repos_uuid());
8698 assert_eq!(duped.repos_root_url(), status.repos_root_url());
8699 assert_eq!(duped.kind(), status.kind());
8700 assert_eq!(duped.depth(), status.depth());
8701 assert_eq!(duped.filesize(), status.filesize());
8702 }
8703
8704 #[test]
8705 fn test_check_root_wc_root() {
8706 let fixture = SvnTestFixture::new();
8707
8708 let mut wc_ctx = Context::new().unwrap();
8709 let (is_wcroot, is_switched, kind) = wc_ctx
8710 .check_root(&fixture.wc_path)
8711 .expect("check_root() should succeed on the WC root");
8712
8713 assert!(is_wcroot, "WC root directory should be reported as wcroot");
8714 assert!(!is_switched, "fresh checkout should not be switched");
8715 assert_eq!(kind, crate::NodeKind::Dir);
8716 }
8717
8718 #[test]
8719 fn test_check_root_non_root_file() {
8720 let mut fixture = SvnTestFixture::new();
8721 let file_path = fixture.add_file("hello.txt", "hello\n");
8722 fixture.commit();
8723
8724 let mut wc_ctx = Context::new().unwrap();
8725 let (is_wcroot, _is_switched, kind) = wc_ctx
8726 .check_root(&file_path)
8727 .expect("check_root() should succeed on a versioned file");
8728
8729 assert!(!is_wcroot, "a file is not a wcroot");
8730 assert_eq!(kind, crate::NodeKind::File);
8731 }
8732
8733 #[test]
8734 fn test_restore_missing_file() {
8735 let mut fixture = SvnTestFixture::new();
8736 let file_path = fixture.add_file("hello.txt", "hello\n");
8737 fixture.commit();
8738
8739 std::fs::remove_file(&file_path).unwrap();
8741
8742 let mut wc_ctx = Context::new().unwrap();
8743 wc_ctx
8744 .restore(&file_path, false)
8745 .expect("restore() should succeed for a missing versioned file");
8746
8747 assert!(
8748 file_path.exists(),
8749 "file should be restored on disk after restore()"
8750 );
8751 assert_eq!(
8752 std::fs::read_to_string(&file_path).unwrap(),
8753 "hello\n",
8754 "restored file should have original content"
8755 );
8756 }
8757
8758 #[test]
8759 fn test_get_ignores_returns_patterns() {
8760 let fixture = SvnTestFixture::new();
8761
8762 let mut wc_ctx = Context::new().unwrap();
8763 let patterns = wc_ctx
8764 .get_ignores(&fixture.wc_path)
8765 .expect("get_ignores() should succeed on a valid WC directory");
8766
8767 assert!(
8770 !patterns.is_empty(),
8771 "expected at least some default ignore patterns"
8772 );
8773 assert!(
8775 patterns.iter().any(|p| p == "*.o"),
8776 "expected '*.o' in default ignore patterns, got: {:?}",
8777 patterns
8778 );
8779 }
8780
8781 #[test]
8782 fn test_get_ignores_includes_svn_ignore_property() {
8783 let mut fixture = SvnTestFixture::new();
8784 fixture.commit();
8785
8786 let wc_path_str = fixture.wc_path_str().to_owned();
8788 fixture
8789 .client_ctx
8790 .propset(
8791 "svn:ignore",
8792 Some(b"my-custom-pattern\n"),
8793 &wc_path_str,
8794 &crate::client::PropSetOptions::default(),
8795 )
8796 .expect("propset svn:ignore should succeed");
8797
8798 let mut wc_ctx = Context::new().unwrap();
8799 let patterns = wc_ctx
8800 .get_ignores(&fixture.wc_path)
8801 .expect("get_ignores() should succeed");
8802
8803 assert!(
8804 patterns.iter().any(|p| p == "my-custom-pattern"),
8805 "expected custom pattern in ignore list, got: {:?}",
8806 patterns
8807 );
8808 }
8809
8810 #[test]
8811 fn test_canonicalize_svn_prop_executable() {
8812 let result = canonicalize_svn_prop(
8814 "svn:executable",
8815 b"yes",
8816 "/some/path",
8817 crate::NodeKind::File,
8818 )
8819 .expect("canonicalize should succeed for svn:executable");
8820 assert_eq!(result, b"*");
8821 }
8822
8823 #[test]
8824 fn test_canonicalize_svn_prop_ignore_adds_newline() {
8825 let result = canonicalize_svn_prop("svn:ignore", b"*.o", "/some/dir", crate::NodeKind::Dir)
8827 .expect("canonicalize should succeed for svn:ignore");
8828 assert_eq!(result, b"*.o\n");
8829 }
8830
8831 #[test]
8832 fn test_canonicalize_svn_prop_keywords_strips_whitespace() {
8833 let result = canonicalize_svn_prop(
8834 "svn:keywords",
8835 b" Rev Author ",
8836 "/some/path",
8837 crate::NodeKind::File,
8838 )
8839 .expect("canonicalize should succeed for svn:keywords");
8840 assert_eq!(result, b"Rev Author");
8841 }
8842
8843 #[test]
8844 fn test_canonicalize_svn_prop_invalid_prop_errors() {
8845 let result = canonicalize_svn_prop(
8847 "svn:ignore",
8848 b"*.o\n",
8849 "/some/path",
8850 crate::NodeKind::File, );
8852 assert!(
8853 result.is_err(),
8854 "svn:ignore on a file should produce a validation error"
8855 );
8856 }
8857
8858 #[test]
8859 fn test_get_default_ignores_returns_patterns() {
8860 let patterns = get_default_ignores().expect("get_default_ignores() should succeed");
8862
8863 assert!(
8866 !patterns.is_empty(),
8867 "expected at least some default ignore patterns"
8868 );
8869 assert!(
8871 patterns.iter().any(|p| p == "*.o"),
8872 "expected '*.o' in default ignore patterns, got: {:?}",
8873 patterns
8874 );
8875 }
8876
8877 #[test]
8878 fn test_get_default_ignores_does_not_include_svn_ignore() {
8879 let mut fixture = SvnTestFixture::new();
8882 fixture.commit();
8883
8884 let wc_path_str = fixture.wc_path_str().to_owned();
8886 fixture
8887 .client_ctx
8888 .propset(
8889 "svn:ignore",
8890 Some(b"my-custom-pattern\n"),
8891 &wc_path_str,
8892 &crate::client::PropSetOptions::default(),
8893 )
8894 .expect("propset svn:ignore should succeed");
8895
8896 let patterns = get_default_ignores().expect("get_default_ignores() should succeed");
8897
8898 assert!(
8899 !patterns.iter().any(|p| p == "my-custom-pattern"),
8900 "get_default_ignores() must not include svn:ignore property values"
8901 );
8902 }
8903
8904 #[test]
8905 fn test_remove_from_revision_control_requires_write_lock() {
8906 let mut fixture = SvnTestFixture::new();
8910 let file_path = fixture.add_file("versioned.txt", "content\n");
8911 fixture.commit();
8912
8913 let mut wc_ctx = Context::new().unwrap();
8914 let result = wc_ctx.remove_from_revision_control(&file_path, false, false);
8915
8916 assert!(
8917 result.is_err(),
8918 "remove_from_revision_control() must fail without a write lock"
8919 );
8920 let err_msg = format!("{:?}", result.unwrap_err());
8921 assert!(
8922 err_msg.to_lowercase().contains("lock") || err_msg.to_lowercase().contains("write"),
8923 "expected a write-lock error, got: {}",
8924 err_msg
8925 );
8926 }
8927
8928 #[test]
8929 fn test_lock_new_with_path_and_token() {
8930 let lock = Lock::new(Some("/trunk/file.txt"), Some(b"opaquelocktoken:abc123"));
8931
8932 assert_eq!(lock.path(), Some("/trunk/file.txt"));
8933 assert_eq!(lock.token(), Some("opaquelocktoken:abc123"));
8934 }
8935
8936 #[test]
8937 fn test_lock_new_with_none() {
8938 let lock = Lock::new(None, None);
8939
8940 assert_eq!(lock.path(), None);
8941 assert_eq!(lock.token(), None);
8942 assert_eq!(lock.owner(), None);
8943 assert_eq!(lock.comment(), None);
8944 }
8945
8946 #[test]
8947 fn test_lock_new_with_path_only() {
8948 let lock = Lock::new(Some("/trunk/file.txt"), None);
8949
8950 assert_eq!(lock.path(), Some("/trunk/file.txt"));
8951 assert_eq!(lock.token(), None);
8952 }
8953
8954 #[test]
8955 #[allow(deprecated)]
8956 fn test_adm_relocate_without_validator() {
8957 let fixture = SvnTestFixture::new();
8958 let old_url = fixture.url.clone();
8959
8960 let (_repos_path2, new_url) = create_repo(fixture.temp_dir.path(), "repos2");
8962
8963 let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
8965
8966 let result = adm.relocate(fixture.wc_path_str(), &old_url, &new_url, true, None);
8968 assert!(
8969 result.is_ok(),
8970 "Adm::relocate() with no validator should succeed: {:?}",
8971 result.err()
8972 );
8973 }
8974
8975 #[test]
8976 #[allow(deprecated)]
8977 fn test_adm_relocate_with_validator() {
8978 let fixture = SvnTestFixture::new();
8979 let old_url = fixture.url.clone();
8980
8981 let (_repos_path2, new_url) = create_repo(fixture.temp_dir.path(), "repos2");
8983
8984 let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
8986
8987 let validator = |_uuid: &str,
8989 _url: &str,
8990 _root_url: &str|
8991 -> Result<(), crate::Error<'static>> { Ok(()) };
8992 let result = adm.relocate(
8993 fixture.wc_path_str(),
8994 &old_url,
8995 &new_url,
8996 true,
8997 Some(&validator),
8998 );
8999 assert!(
9000 result.is_ok(),
9001 "Adm::relocate() with validator should succeed: {:?}",
9002 result.err()
9003 );
9004 }
9005
9006 #[test]
9007 #[allow(deprecated)]
9008 fn test_adm_close() {
9009 let fixture = SvnTestFixture::new();
9010 let mut adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
9011 adm.close();
9012 }
9014
9015 #[test]
9016 #[allow(deprecated)]
9017 fn test_adm_close_idempotent() {
9018 let fixture = SvnTestFixture::new();
9019 let mut adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
9020 adm.close();
9021 adm.close(); }
9023
9024 #[test]
9025 #[allow(deprecated)]
9026 fn test_adm_queue_committed() {
9027 let mut fixture = SvnTestFixture::new();
9028 fixture.add_file("test.txt", "hello");
9029 fixture.commit();
9030
9031 let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
9032 let mut queue = CommittedQueue::new();
9033
9034 let file_path = fixture.wc_path.join("test.txt");
9035 let result = adm.queue_committed(
9036 file_path.to_str().unwrap(),
9037 &mut queue,
9038 false,
9039 false,
9040 false,
9041 None,
9042 );
9043 assert!(result.is_ok(), "queue_committed failed: {:?}", result.err());
9044 }
9045
9046 #[test]
9047 #[allow(deprecated)]
9048 fn test_adm_queue_committed_with_digest() {
9049 let mut fixture = SvnTestFixture::new();
9050 fixture.add_file("test.txt", "hello");
9051 fixture.commit();
9052
9053 let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
9054 let mut queue = CommittedQueue::new();
9055 let digest: [u8; 16] = [0; 16];
9056
9057 let file_path = fixture.wc_path.join("test.txt");
9058 let result = adm.queue_committed(
9059 file_path.to_str().unwrap(),
9060 &mut queue,
9061 false,
9062 false,
9063 false,
9064 Some(&digest),
9065 );
9066 assert!(
9067 result.is_ok(),
9068 "queue_committed with digest failed: {:?}",
9069 result.err()
9070 );
9071 }
9072
9073 #[test]
9074 #[allow(deprecated)]
9075 fn test_adm_process_committed_queue() {
9076 let mut fixture = SvnTestFixture::new();
9077 fixture.add_file("test.txt", "hello");
9078 fixture.commit();
9079
9080 let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
9081 let mut queue = CommittedQueue::new();
9082
9083 let file_path = fixture.wc_path.join("test.txt");
9084 adm.queue_committed(
9085 file_path.to_str().unwrap(),
9086 &mut queue,
9087 false,
9088 false,
9089 false,
9090 None,
9091 )
9092 .unwrap();
9093
9094 let result = adm.process_committed_queue(
9095 &mut queue,
9096 crate::Revnum(2),
9097 Some("2026-03-24T00:00:00.000000Z"),
9098 Some("testuser"),
9099 );
9100 assert!(
9101 result.is_ok(),
9102 "process_committed_queue failed: {:?}",
9103 result.err()
9104 );
9105 }
9106
9107 #[test]
9108 #[allow(deprecated)]
9109 fn test_adm_process_committed_queue_no_date_or_author() {
9110 let mut fixture = SvnTestFixture::new();
9111 fixture.add_file("test.txt", "hello");
9112 fixture.commit();
9113
9114 let adm = Adm::open(fixture.wc_path_str(), true, -1).unwrap();
9115 let mut queue = CommittedQueue::new();
9116
9117 let file_path = fixture.wc_path.join("test.txt");
9118 adm.queue_committed(
9119 file_path.to_str().unwrap(),
9120 &mut queue,
9121 false,
9122 false,
9123 false,
9124 None,
9125 )
9126 .unwrap();
9127
9128 let result = adm.process_committed_queue(&mut queue, crate::Revnum(2), None, None);
9129 assert!(
9130 result.is_ok(),
9131 "process_committed_queue with no date/author failed: {:?}",
9132 result.err()
9133 );
9134 }
9135}