use std::ffi::{CStr, CString, c_char}; use std::mem::MaybeUninit; use std::ptr; use thiserror::Error; // Error deriving /// Errors #[derive(Error, Debug)] pub enum FslError { #[error("context init error: {0}")] CxInitError(i32), #[error("checkin commit error: {0}")] CheckinCommitError(i32), #[error("checkout open dir error: {0}")] CkoutOpenDirError(i32), #[error("db repo error: {0}")] DbRepoError(i32), #[error("manage counts error: {0}")] ManageCountsError(i32), #[error("unmanage counts error: {0}")] UnmanageCountsError(i32), /// FFI errors #[error(transparent)] NulError(#[from] std::ffi::NulError), #[error(transparent)] Utf8Error(#[from] std::str::Utf8Error), } type Result = core::result::Result; type RID = i32; type UUID = String; /// Wrapper around libfossil_rs_ffi::fsl_cx pub struct FslCx { f: *mut libfossil_rs_ffi::fsl_cx, } impl Drop for FslCx { fn drop(&mut self) { unsafe { libfossil_rs_ffi::fsl_cx_finalize(self.f) } } } /// Wrapper around libfossil_rs_ffi::fsl_db pub struct FslDb { fsl_db: libfossil_rs_ffi::fsl_db, } /// When caling ckout_mangage_state, you get a counts in response #[derive(Debug)] pub struct FslManageCounts { pub added: u32, pub updated: u32, pub skipped: u32, } impl FslCx { pub fn new() -> Result { let mut f: *mut libfossil_rs_ffi::fsl_cx = ptr::null_mut(); let ret = unsafe { libfossil_rs_ffi::fsl_cx_init(ptr::from_mut(&mut f), ptr::null()) }; if ret > 0 { Err(FslError::CxInitError(ret).into()) } else { Ok(Self { f }) } } /// opens the checkout directory pub fn open_checkout_dir(&self, path: &str) -> Result<()> { let p = CString::new(path)?; let ret = unsafe { libfossil_rs_ffi::fsl_ckout_open_dir(self.f, p.as_ptr(), false) }; if ret > 0 { Err(FslError::CkoutOpenDirError(ret).into()) } else { Ok(()) } } /// returns associated FslDb pub fn db_repo(&self) -> Result { let fsl_db = unsafe { *libfossil_rs_ffi::fsl_cx_db_repo(self.f) }; Ok(FslDb { fsl_db }) } /// adds given filename or directory (recursively) to the current /// checkout pub fn checkout_manage(&self, filename: &str, relative_to_cwd: bool, check_ignore_globs: bool) -> Result { // println!("[checkout_manage] filename: {filename}"); let c_filename = CString::new(filename)?; let mut ckout_manage_opt = unsafe { libfossil_rs_ffi::fsl_ckout_manage_opt { filename: c_filename.as_ptr(), relativeToCwd: relative_to_cwd, checkIgnoreGlobs: check_ignore_globs, callback: None, callbackState: ptr::null_mut(), counts: libfossil_rs_ffi::fsl_ckout_manage_opt_empty.counts, } }; let ret = unsafe { libfossil_rs_ffi::fsl_ckout_manage(self.f, ptr::from_mut(&mut ckout_manage_opt)) }; // println!("[checkout_manage] ret: {ret}"); if ret > 0 { // TODO Why stack overflow? Err(FslError::ManageCountsError(ret).into()) // Err(anyhow::anyhow!("Some checkout error")) } else { Ok(FslManageCounts { added: ckout_manage_opt.counts.added, updated: ckout_manage_opt.counts.updated, skipped: ckout_manage_opt.counts.skipped, }) } } /// removes given filename or directory (recursively) from the /// current checkout pub fn checkout_unmanage(&self, filename: &str, relative_to_cwd: bool, scan_for_changes: bool) -> Result<()> { let c_filename = CString::new(filename)?; let mut ckout_unmanage_opt = libfossil_rs_ffi::fsl_ckout_unmanage_opt { filename: c_filename.as_ptr(), vfileIds: ptr::null(), relativeToCwd: relative_to_cwd, scanForChanges: scan_for_changes, callback: None, callbackState: ptr::null_mut(), }; let ret = unsafe { libfossil_rs_ffi::fsl_ckout_unmanage(self.f, ptr::from_mut(&mut ckout_unmanage_opt)) }; if ret > 0 { Err(FslError::UnmanageCountsError(ret).into()) } else { Ok(()) } } /// This creates and saves a "checkin manifest" pub fn checkin_commit(&self, message: &str) -> Result<(RID, UUID)> { let c_message = CString::new(message)?; let checkin_opt = libfossil_rs_ffi::fsl_checkin_opt { message: c_message.as_ptr(), messageMimeType: ptr::null(), user: ptr::null(), // default user from fsl_cx_user_get branch: ptr::null(), // if not null, starts a new branch with this name bgColor: ptr::null(), // bgcolor propagating property of the (non-null) branch isPrivate: false, calcRCard: true, // this is an additional security, but can be expensive for large repos integrate: true, // close merged-in branches allowMergeConflict: false, // don't allow to merge in conflicts scanForChanges: true, // set to false ONLY when calling code calls fsl_ckout_changes_scan rmRemovedFiles: false, // not yet implemented in libfossil deltaPolicy: -1, // decide for delta based on some heuristics julianTime: 0.0, // when 0, time of fsl_checkin_commit call is used closeBranch: ptr::null(), // when null, commited manifest will include a tag that closes a branch dumpManifestFile: ptr::null(), // dump generated manifest to a file }; // TODO I don't know how to handle UUID here, it's of *mut *mut c_char type. // I guess this means it should be instantiated with current // UUID first, then allow fsl_checkin_commit to mutate it to // new UUID? let mut rid = 0; // let mut uuid = CString::new("")?; // let mut uuid: MaybeUninit<*mut c_char> = MaybeUninit::uninit(); let ret = unsafe { libfossil_rs_ffi::fsl_checkin_commit( self.f, ptr::from_ref(&checkin_opt), ptr::from_mut(&mut rid), // if this is non-null, it is assigned new checkin's RID value ptr::null_mut(), // uuid.as_mut_ptr(), // if this is non-null, it is assigned new checkin's UUID value // (RID and UUID can be fetched later using fsl_ckout_version_info) ) }; // println!("[checkin_comimt] ret: {ret}"); // let uuid: *mut c_char = unsafe { // uuid.assume_init() // }; // println!("[checkin_commit] after assume_init"); // let uuid = unsafe { // CStr::from_ptr(uuid).to_str()?.to_owned() // }; // println!("[checkin_commit] after CStr"); if ret > 0 { Err(FslError::CheckinCommitError(ret).into()) } else { Ok((rid, "".to_owned())) } } pub fn checkout_version_info(&self) -> Result<(RID, UUID)> { let mut rid = 0; // let uuid = CString::new("")?; let mut uuid: MaybeUninit<*const c_char> = MaybeUninit::uninit(); unsafe { libfossil_rs_ffi::fsl_ckout_version_info( self.f, ptr::from_mut(&mut rid), uuid.as_mut_ptr(), // ptr::from_mut(&mut uuid.as_ptr()), ); }; let uuid: *const c_char = unsafe { uuid.assume_init() }; let uuid = unsafe { CStr::from_ptr(uuid).to_str()?.to_owned() }; Ok((rid, uuid)) } /// Useful in debugging the underlying libfossil lib /// TODO Customize cx.output to return a String here pub fn cx_err_report(&self) -> Result<()> { unsafe { libfossil_rs_ffi::fsl_cx_err_report(self.f, false) }; Ok(()) } // Private functions } impl FslDb { pub fn get_filename(&self) -> Result { unsafe { Ok(CStr::from_ptr(self.fsl_db.filename).to_str()?.to_owned()) } } }