use crate::{svn_result, Error, Revision, Revnum};
use std::collections::HashMap;
use std::marker::PhantomData;
use subversion_sys::svn_mergeinfo_t;
pub struct Mergeinfo {
ptr: svn_mergeinfo_t,
pool: apr::Pool<'static>,
_phantom: PhantomData<*mut ()>, }
impl Mergeinfo {
pub(crate) unsafe fn from_ptr_and_pool(ptr: svn_mergeinfo_t, pool: apr::Pool<'static>) -> Self {
Self {
ptr,
pool,
_phantom: PhantomData,
}
}
pub fn parse(input: &str) -> Result<Self, Error<'static>> {
let pool = apr::Pool::new();
let input_cstr = std::ffi::CString::new(input)
.map_err(|_| Error::from_message("Invalid mergeinfo string"))?;
unsafe {
let mut mergeinfo: svn_mergeinfo_t = std::ptr::null_mut();
let err = subversion_sys::svn_mergeinfo_parse(
&mut mergeinfo,
input_cstr.as_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Self::from_ptr_and_pool(mergeinfo, pool))
}
}
pub fn to_string(&self) -> Result<String, Error<'static>> {
unsafe {
let mut output: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
let err = subversion_sys::svn_mergeinfo_to_string(
&mut output,
self.ptr,
self.pool.as_mut_ptr(),
);
svn_result(err)?;
if output.is_null() || (*output).data.is_null() {
return Ok(String::new());
}
let data_slice = std::slice::from_raw_parts((*output).data as *const u8, (*output).len);
Ok(std::str::from_utf8(data_slice)
.map_err(|_| Error::from_message("Mergeinfo string is not valid UTF-8"))?
.to_string())
}
}
pub fn merge(&mut self, other: &Mergeinfo) -> Result<(), Error<'static>> {
unsafe {
let err = subversion_sys::svn_mergeinfo_merge2(
self.ptr,
other.ptr,
self.pool.as_mut_ptr(),
self.pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn remove(&self, eraser: &Mergeinfo) -> Result<Mergeinfo, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut result: svn_mergeinfo_t = std::ptr::null_mut();
let err = subversion_sys::svn_mergeinfo_remove2(
&mut result,
eraser.ptr,
self.ptr,
1, pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Mergeinfo::from_ptr_and_pool(result, pool))
}
}
pub fn intersect(&self, other: &Mergeinfo) -> Result<Mergeinfo, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut result: svn_mergeinfo_t = std::ptr::null_mut();
let err = subversion_sys::svn_mergeinfo_intersect2(
&mut result,
self.ptr,
other.ptr,
1, pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Mergeinfo::from_ptr_and_pool(result, pool))
}
}
pub fn diff(&self, other: &Mergeinfo) -> Result<(Mergeinfo, Mergeinfo), Error<'static>> {
let pool1 = apr::Pool::new();
let pool2 = apr::Pool::new();
unsafe {
let mut deleted: svn_mergeinfo_t = std::ptr::null_mut();
let mut added: svn_mergeinfo_t = std::ptr::null_mut();
let err = subversion_sys::svn_mergeinfo_diff2(
&mut deleted,
&mut added,
self.ptr,
other.ptr,
1, pool1.as_mut_ptr(),
pool1.as_mut_ptr(),
);
svn_result(err)?;
Ok((
Mergeinfo::from_ptr_and_pool(deleted, pool1),
Mergeinfo::from_ptr_and_pool(added, pool2),
))
}
}
pub fn dup(&self) -> Mergeinfo {
let pool = apr::Pool::new();
unsafe {
let result = subversion_sys::svn_mergeinfo_dup(self.ptr, pool.as_mut_ptr());
Mergeinfo::from_ptr_and_pool(result, pool)
}
}
pub fn sort(&mut self) -> Result<(), Error<'static>> {
unsafe {
let err = subversion_sys::svn_mergeinfo_sort(self.ptr, self.pool.as_mut_ptr());
svn_result(err)
}
}
pub fn inheritable(
&self,
path: Option<&str>,
start: Revnum,
end: Revnum,
inheritable: bool,
) -> Result<Mergeinfo, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let path_cstr = path
.map(|p| std::ffi::CString::new(p).expect("path contains null byte"))
.map(|c| c.as_ptr())
.unwrap_or(std::ptr::null());
let mut result: svn_mergeinfo_t = std::ptr::null_mut();
let err = subversion_sys::svn_mergeinfo_inheritable2(
&mut result,
self.ptr,
path_cstr,
start.0,
end.0,
inheritable as i32,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Mergeinfo::from_ptr_and_pool(result, pool))
}
}
pub fn paths(&self) -> HashMap<String, Vec<crate::RevisionRange>> {
let mut result = HashMap::new();
if self.ptr.is_null() {
return result;
}
unsafe {
let hash =
apr::hash::TypedHash::<subversion_sys::svn_rangelist_t>::from_ptr(self.ptr as _);
for (key, rangelist) in hash.iter() {
let path = std::str::from_utf8(key)
.expect("mergeinfo path is not valid UTF-8")
.to_string();
let mut ranges = Vec::new();
let array = apr::tables::TypedArray::<subversion_sys::svn_merge_range_t>::from_ptr(
rangelist as *const _ as *mut _,
);
for range in array.iter() {
ranges.push(crate::RevisionRange {
start: Revision::Number(Revnum(range.start)),
end: Revision::Number(Revnum(range.end)),
});
}
result.insert(path, ranges);
}
}
result
}
}
impl Drop for Mergeinfo {
fn drop(&mut self) {
}
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum MergeinfoInheritance {
Explicit,
Inherited,
NearestAncestor,
}
impl From<subversion_sys::svn_mergeinfo_inheritance_t> for MergeinfoInheritance {
fn from(value: subversion_sys::svn_mergeinfo_inheritance_t) -> Self {
match value {
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor => {
MergeinfoInheritance::NearestAncestor
}
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit => {
MergeinfoInheritance::Explicit
}
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited => {
MergeinfoInheritance::Inherited
}
_ => unreachable!(),
}
}
}
impl From<MergeinfoInheritance> for subversion_sys::svn_mergeinfo_inheritance_t {
fn from(value: MergeinfoInheritance) -> Self {
match value {
MergeinfoInheritance::NearestAncestor => {
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor
}
MergeinfoInheritance::Explicit => {
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit
}
MergeinfoInheritance::Inherited => {
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited
}
}
}
}
impl MergeinfoInheritance {
pub fn to_word(self) -> &'static str {
unsafe {
let ptr = subversion_sys::svn_inheritance_to_word(self.into());
std::ffi::CStr::from_ptr(ptr)
.to_str()
.expect("inheritance word is not valid UTF-8")
}
}
pub fn from_word(word: &str) -> Self {
let word_cstr = std::ffi::CString::new(word).expect("inheritance word contains null byte");
unsafe { subversion_sys::svn_inheritance_from_word(word_cstr.as_ptr()).into() }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MergeRange {
pub start: Revnum,
pub end: Revnum,
pub inheritable: bool,
}
impl MergeRange {
pub fn new(start: Revnum, end: Revnum, inheritable: bool) -> Self {
Self {
start,
end,
inheritable,
}
}
}
impl From<&subversion_sys::svn_merge_range_t> for MergeRange {
fn from(r: &subversion_sys::svn_merge_range_t) -> Self {
Self {
start: Revnum(r.start),
end: Revnum(r.end),
inheritable: r.inheritable != 0,
}
}
}
impl From<subversion_sys::svn_merge_range_t> for MergeRange {
fn from(r: subversion_sys::svn_merge_range_t) -> Self {
Self {
start: Revnum(r.start),
end: Revnum(r.end),
inheritable: r.inheritable != 0,
}
}
}
pub struct Rangelist {
ptr: *mut subversion_sys::svn_rangelist_t,
pool: apr::Pool<'static>,
}
impl Rangelist {
pub fn new() -> Self {
let pool = apr::Pool::new();
let ptr = unsafe {
apr_sys::apr_array_make(
pool.as_mut_ptr(),
0,
std::mem::size_of::<subversion_sys::svn_merge_range_t>() as i32,
)
};
Self { ptr, pool }
}
pub(crate) unsafe fn from_ptr_and_pool(
ptr: *mut subversion_sys::svn_rangelist_t,
pool: apr::Pool<'static>,
) -> Self {
Self { ptr, pool }
}
pub fn to_string(&self) -> Result<String, Error<'static>> {
unsafe {
let mut output: *mut subversion_sys::svn_string_t = std::ptr::null_mut();
let err = subversion_sys::svn_rangelist_to_string(
&mut output,
self.ptr,
self.pool.as_mut_ptr(),
);
svn_result(err)?;
if output.is_null() || (*output).data.is_null() {
return Ok(String::new());
}
let data_slice = std::slice::from_raw_parts((*output).data as *const u8, (*output).len);
Ok(std::str::from_utf8(data_slice)
.map_err(|_| Error::from_message("Rangelist string is not valid UTF-8"))?
.to_string())
}
}
pub fn diff(
from: &Rangelist,
to: &Rangelist,
consider_inheritance: bool,
) -> Result<(Rangelist, Rangelist), Error<'static>> {
let pool1 = apr::Pool::new();
let pool2 = apr::Pool::new();
unsafe {
let mut deleted: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
let mut added: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
let err = subversion_sys::svn_rangelist_diff(
&mut deleted,
&mut added,
from.ptr,
to.ptr,
consider_inheritance.into(),
pool1.as_mut_ptr(),
);
svn_result(err)?;
Ok((
Rangelist::from_ptr_and_pool(deleted, pool1),
Rangelist::from_ptr_and_pool(added, pool2),
))
}
}
pub fn merge(&mut self, changes: &Rangelist) -> Result<(), Error<'static>> {
unsafe {
let err = subversion_sys::svn_rangelist_merge2(
self.ptr,
changes.ptr,
self.pool.as_mut_ptr(),
self.pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn remove(
eraser: &Rangelist,
whiteboard: &Rangelist,
consider_inheritance: bool,
) -> Result<Rangelist, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut output: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
let err = subversion_sys::svn_rangelist_remove(
&mut output,
eraser.ptr,
whiteboard.ptr,
consider_inheritance.into(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Rangelist::from_ptr_and_pool(output, pool))
}
}
pub fn intersect(
rangelist1: &Rangelist,
rangelist2: &Rangelist,
consider_inheritance: bool,
) -> Result<Rangelist, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut output: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
let err = subversion_sys::svn_rangelist_intersect(
&mut output,
rangelist1.ptr,
rangelist2.ptr,
consider_inheritance.into(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Rangelist::from_ptr_and_pool(output, pool))
}
}
pub fn reverse(&mut self) -> Result<(), Error<'static>> {
unsafe {
let err = subversion_sys::svn_rangelist_reverse(self.ptr, self.pool.as_mut_ptr());
svn_result(err)
}
}
pub fn dup(&self) -> Rangelist {
let pool = apr::Pool::new();
unsafe {
let result = subversion_sys::svn_rangelist_dup(self.ptr, pool.as_mut_ptr());
Rangelist::from_ptr_and_pool(result, pool)
}
}
pub fn ranges(&self) -> Vec<MergeRange> {
if self.ptr.is_null() {
return Vec::new();
}
unsafe {
let array =
apr::tables::TypedArray::<subversion_sys::svn_merge_range_t>::from_ptr(self.ptr);
array.iter().map(MergeRange::from).collect()
}
}
pub fn is_empty(&self) -> bool {
if self.ptr.is_null() {
return true;
}
unsafe { (*self.ptr).nelts == 0 }
}
pub fn len(&self) -> usize {
if self.ptr.is_null() {
return 0;
}
unsafe { (*self.ptr).nelts as usize }
}
pub fn inheritable(
&self,
start: Revnum,
end: Revnum,
inheritable: bool,
) -> Result<Rangelist, Error<'static>> {
let pool = apr::Pool::new();
unsafe {
let mut result: *mut subversion_sys::svn_rangelist_t = std::ptr::null_mut();
let err = subversion_sys::svn_rangelist_inheritable2(
&mut result,
self.ptr,
start.0,
end.0,
inheritable as i32,
pool.as_mut_ptr(),
pool.as_mut_ptr(),
);
svn_result(err)?;
Ok(Rangelist::from_ptr_and_pool(result, pool))
}
}
}
impl Default for Rangelist {
fn default() -> Self {
Self::new()
}
}
pub struct MergeinfoCatalog {
ptr: subversion_sys::svn_mergeinfo_catalog_t,
pool: apr::Pool<'static>,
}
impl MergeinfoCatalog {
pub fn new() -> Self {
let pool = apr::Pool::new();
let ptr = unsafe { apr_sys::apr_hash_make(pool.as_mut_ptr()) };
Self { ptr, pool }
}
pub(crate) unsafe fn from_ptr_and_pool(
ptr: subversion_sys::svn_mergeinfo_catalog_t,
pool: apr::Pool<'static>,
) -> Self {
Self { ptr, pool }
}
pub fn merge(&mut self, changes: &MergeinfoCatalog) -> Result<(), Error<'static>> {
unsafe {
let err = subversion_sys::svn_mergeinfo_catalog_merge(
self.ptr,
changes.ptr,
self.pool.as_mut_ptr(),
self.pool.as_mut_ptr(),
);
svn_result(err)
}
}
pub fn dup(&self) -> MergeinfoCatalog {
let pool = apr::Pool::new();
unsafe {
let result = subversion_sys::svn_mergeinfo_catalog_dup(self.ptr, pool.as_mut_ptr());
MergeinfoCatalog::from_ptr_and_pool(result, pool)
}
}
pub fn is_empty(&self) -> bool {
if self.ptr.is_null() {
return true;
}
unsafe { apr_sys::apr_hash_count(self.ptr) == 0 }
}
pub fn len(&self) -> usize {
if self.ptr.is_null() {
return 0;
}
unsafe { apr_sys::apr_hash_count(self.ptr) as usize }
}
pub fn paths(&self) -> Vec<String> {
if self.ptr.is_null() {
return Vec::new();
}
unsafe {
let hash = apr::hash::TypedHash::<svn_mergeinfo_t>::from_ptr(self.ptr);
hash.iter()
.map(|(key, _)| {
std::str::from_utf8(key)
.expect("catalog path is not valid UTF-8")
.to_string()
})
.collect()
}
}
}
impl Default for MergeinfoCatalog {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mergeinfo_parse() {
let mergeinfo = Mergeinfo::parse("/trunk:1-10").unwrap();
let s = mergeinfo.to_string().unwrap();
assert_eq!(s, "/trunk:1-10");
}
#[test]
fn test_mergeinfo_inheritance_conversion() {
assert_eq!(
MergeinfoInheritance::from(
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit
),
MergeinfoInheritance::Explicit
);
assert_eq!(
MergeinfoInheritance::from(
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited
),
MergeinfoInheritance::Inherited
);
assert_eq!(
MergeinfoInheritance::from(
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor
),
MergeinfoInheritance::NearestAncestor
);
}
#[test]
fn test_mergeinfo_inheritance_to_svn() {
assert_eq!(
subversion_sys::svn_mergeinfo_inheritance_t::from(MergeinfoInheritance::Explicit),
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_explicit
);
assert_eq!(
subversion_sys::svn_mergeinfo_inheritance_t::from(MergeinfoInheritance::Inherited),
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_inherited
);
assert_eq!(
subversion_sys::svn_mergeinfo_inheritance_t::from(
MergeinfoInheritance::NearestAncestor
),
subversion_sys::svn_mergeinfo_inheritance_t_svn_mergeinfo_nearest_ancestor
);
}
#[test]
fn test_mergeinfo_roundtrip_conversion() {
let variants = vec![
MergeinfoInheritance::Explicit,
MergeinfoInheritance::Inherited,
MergeinfoInheritance::NearestAncestor,
];
for variant in variants {
let svn_val = subversion_sys::svn_mergeinfo_inheritance_t::from(variant);
let back = MergeinfoInheritance::from(svn_val);
assert_eq!(variant, back);
}
}
#[test]
fn test_mergeinfo_drop() {
{
let _mergeinfo = Mergeinfo::parse("/trunk:1-5").unwrap();
}
}
#[test]
fn test_mergeinfo_dup() {
let mergeinfo = Mergeinfo::parse("/trunk:1-10").unwrap();
let dup = mergeinfo.dup();
assert_eq!(dup.to_string().unwrap(), "/trunk:1-10");
}
#[test]
fn test_rangelist_new() {
let rangelist = Rangelist::new();
assert!(rangelist.is_empty());
assert_eq!(rangelist.len(), 0);
}
#[test]
fn test_merge_range_from() {
let range = MergeRange {
start: Revnum(1),
end: Revnum(10),
inheritable: true,
};
assert_eq!(range.start.0, 1);
assert_eq!(range.end.0, 10);
assert!(range.inheritable);
}
#[test]
fn test_rangelist_dup() {
let rangelist = Rangelist::new();
let dup = rangelist.dup();
assert!(dup.is_empty());
}
#[test]
fn test_rangelist_to_string_empty() {
let rangelist = Rangelist::new();
let s = rangelist.to_string().unwrap();
assert_eq!(s, "");
}
#[test]
fn test_rangelist_merge() {
let mut r1 = Rangelist::new();
let r2 = Rangelist::new();
r1.merge(&r2).unwrap();
assert!(r1.is_empty());
}
#[test]
fn test_rangelist_intersect() {
let r1 = Rangelist::new();
let r2 = Rangelist::new();
let intersected = Rangelist::intersect(&r1, &r2, true).unwrap();
assert!(intersected.is_empty());
}
#[test]
fn test_rangelist_reverse() {
let mut rangelist = Rangelist::new();
rangelist.reverse().unwrap();
assert!(rangelist.is_empty());
}
#[test]
fn test_rangelist_ranges_empty() {
let rangelist = Rangelist::new();
let ranges = rangelist.ranges();
assert!(ranges.is_empty());
}
#[test]
fn test_mergeinfo_catalog_new() {
let catalog = MergeinfoCatalog::new();
assert!(catalog.is_empty());
assert_eq!(catalog.len(), 0);
}
#[test]
fn test_mergeinfo_catalog_dup() {
let catalog = MergeinfoCatalog::new();
let dup = catalog.dup();
assert!(dup.is_empty());
}
#[test]
fn test_mergeinfo_catalog_merge() {
let mut c1 = MergeinfoCatalog::new();
let c2 = MergeinfoCatalog::new();
c1.merge(&c2).unwrap();
assert!(c1.is_empty());
}
#[test]
fn test_mergeinfo_catalog_paths() {
let catalog = MergeinfoCatalog::new();
let paths = catalog.paths();
assert!(paths.is_empty());
}
}