use crate::buf::Buf; use crate::reference::Reference; use crate::repo::Repository; use crate::util::{self, Binding}; use crate::{raw, Error}; use std::os::raw::c_int; use std::path::Path; use std::ptr; use std::str; use std::{marker, mem}; /// An owned git worktree /// /// This structure corresponds to a `git_worktree` in libgit2. // pub struct Worktree { raw: *mut raw::git_worktree, } /// Options which can be used to configure how a worktree is initialized pub struct WorktreeAddOptions<'a> { raw: raw::git_worktree_add_options, _marker: marker::PhantomData>, } /// Options to configure how worktree pruning is performed pub struct WorktreePruneOptions { raw: raw::git_worktree_prune_options, } /// Lock Status of a worktree #[derive(PartialEq, Debug)] pub enum WorktreeLockStatus { /// Worktree is Unlocked Unlocked, /// Worktree is locked with the optional message Locked(Option), } impl Worktree { /// Open a worktree of a the repository /// /// If a repository is not the main tree but a worktree, this /// function will look up the worktree inside the parent /// repository and create a new `git_worktree` structure. pub fn open_from_repository(repo: &Repository) -> Result { let mut raw = ptr::null_mut(); unsafe { try_call!(raw::git_worktree_open_from_repository(&mut raw, repo.raw())); Ok(Binding::from_raw(raw)) } } /// Retrieves the name of the worktree /// /// This is the name that can be passed to repo::Repository::find_worktree /// to reopen the worktree. This is also the name that would appear in the /// list returned by repo::Repository::worktrees pub fn name(&self) -> Option<&str> { unsafe { crate::opt_bytes(self, raw::git_worktree_name(self.raw)) .and_then(|s| str::from_utf8(s).ok()) } } /// Retrieves the path to the worktree /// /// This is the path to the top-level of the source and not the path to the /// .git file within the worktree. This path can be passed to /// repo::Repository::open. pub fn path(&self) -> &Path { unsafe { util::bytes2path(crate::opt_bytes(self, raw::git_worktree_path(self.raw)).unwrap()) } } /// Validates the worktree /// /// This checks that it still exists on the /// filesystem and that the metadata is correct pub fn validate(&self) -> Result<(), Error> { unsafe { try_call!(raw::git_worktree_validate(self.raw)); } Ok(()) } /// Locks the worktree pub fn lock(&self, reason: Option<&str>) -> Result<(), Error> { let reason = crate::opt_cstr(reason)?; unsafe { try_call!(raw::git_worktree_lock(self.raw, reason)); } Ok(()) } /// Unlocks the worktree pub fn unlock(&self) -> Result<(), Error> { unsafe { try_call!(raw::git_worktree_unlock(self.raw)); } Ok(()) } /// Checks if worktree is locked pub fn is_locked(&self) -> Result { let buf = Buf::new(); unsafe { match try_call!(raw::git_worktree_is_locked(buf.raw(), self.raw)) { 0 => Ok(WorktreeLockStatus::Unlocked), _ => { let v = buf.to_vec(); Ok(WorktreeLockStatus::Locked(match v.len() { 0 => None, _ => Some(String::from_utf8(v).unwrap()), })) } } } } /// Prunes the worktree pub fn prune(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<(), Error> { // When successful the worktree should be removed however the backing structure // of the git_worktree should still be valid. unsafe { try_call!(raw::git_worktree_prune(self.raw, opts.map(|o| o.raw()))); } Ok(()) } /// Checks if the worktree is prunable pub fn is_prunable(&self, opts: Option<&mut WorktreePruneOptions>) -> Result { unsafe { let rv = try_call!(raw::git_worktree_is_prunable( self.raw, opts.map(|o| o.raw()) )); Ok(rv != 0) } } } impl<'a> WorktreeAddOptions<'a> { /// Creates a default set of add options. /// /// By default this will not lock the worktree pub fn new() -> WorktreeAddOptions<'a> { unsafe { let mut raw = mem::zeroed(); assert_eq!( raw::git_worktree_add_options_init(&mut raw, raw::GIT_WORKTREE_ADD_OPTIONS_VERSION), 0 ); WorktreeAddOptions { raw, _marker: marker::PhantomData, } } } /// If enabled, this will cause the newly added worktree to be locked pub fn lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> { self.raw.lock = enabled as c_int; self } /// reference to use for the new worktree HEAD pub fn reference( &mut self, reference: Option<&'a Reference<'_>>, ) -> &mut WorktreeAddOptions<'a> { self.raw.reference = if let Some(reference) = reference { reference.raw() } else { ptr::null_mut() }; self } /// Get a set of raw add options to be used with `git_worktree_add` pub fn raw(&self) -> *const raw::git_worktree_add_options { &self.raw } } impl WorktreePruneOptions { /// Creates a default set of pruning options /// /// By defaults this will prune only worktrees that are no longer valid /// unlocked and not checked out pub fn new() -> WorktreePruneOptions { unsafe { let mut raw = mem::zeroed(); assert_eq!( raw::git_worktree_prune_options_init( &mut raw, raw::GIT_WORKTREE_PRUNE_OPTIONS_VERSION ), 0 ); WorktreePruneOptions { raw } } } /// Controls whether valid (still existing on the filesystem) worktrees /// will be pruned /// /// Defaults to false pub fn valid(&mut self, valid: bool) -> &mut WorktreePruneOptions { self.flag(raw::GIT_WORKTREE_PRUNE_VALID, valid) } /// Controls whether locked worktrees will be pruned /// /// Defaults to false pub fn locked(&mut self, locked: bool) -> &mut WorktreePruneOptions { self.flag(raw::GIT_WORKTREE_PRUNE_LOCKED, locked) } /// Controls whether the actual working tree on the filesystem is recursively removed /// /// Defaults to false pub fn working_tree(&mut self, working_tree: bool) -> &mut WorktreePruneOptions { self.flag(raw::GIT_WORKTREE_PRUNE_WORKING_TREE, working_tree) } fn flag(&mut self, flag: raw::git_worktree_prune_t, on: bool) -> &mut WorktreePruneOptions { if on { self.raw.flags |= flag as u32; } else { self.raw.flags &= !(flag as u32); } self } /// Get a set of raw prune options to be used with `git_worktree_prune` pub fn raw(&mut self) -> *mut raw::git_worktree_prune_options { &mut self.raw } } impl Binding for Worktree { type Raw = *mut raw::git_worktree; unsafe fn from_raw(ptr: *mut raw::git_worktree) -> Worktree { Worktree { raw: ptr } } fn raw(&self) -> *mut raw::git_worktree { self.raw } } impl Drop for Worktree { fn drop(&mut self) { unsafe { raw::git_worktree_free(self.raw) } } } #[cfg(test)] mod tests { use crate::WorktreeAddOptions; use crate::WorktreeLockStatus; use tempfile::TempDir; #[test] fn smoke_add_no_ref() { let (_td, repo) = crate::test::repo_init(); let wtdir = TempDir::new().unwrap(); let wt_path = wtdir.path().join("tree-no-ref-dir"); let opts = WorktreeAddOptions::new(); let wt = repo.worktree("tree-no-ref", &wt_path, Some(&opts)).unwrap(); assert_eq!(wt.name(), Some("tree-no-ref")); assert_eq!( wt.path().canonicalize().unwrap(), wt_path.canonicalize().unwrap() ); let status = wt.is_locked().unwrap(); assert_eq!(status, WorktreeLockStatus::Unlocked); } #[test] fn smoke_add_locked() { let (_td, repo) = crate::test::repo_init(); let wtdir = TempDir::new().unwrap(); let wt_path = wtdir.path().join("locked-tree"); let mut opts = WorktreeAddOptions::new(); opts.lock(true); let wt = repo.worktree("locked-tree", &wt_path, Some(&opts)).unwrap(); // shouldn't be able to lock a worktree that was created locked assert!(wt.lock(Some("my reason")).is_err()); assert_eq!(wt.name(), Some("locked-tree")); assert_eq!( wt.path().canonicalize().unwrap(), wt_path.canonicalize().unwrap() ); assert_eq!(wt.is_locked().unwrap(), WorktreeLockStatus::Locked(None)); assert!(wt.unlock().is_ok()); assert!(wt.lock(Some("my reason")).is_ok()); assert_eq!( wt.is_locked().unwrap(), WorktreeLockStatus::Locked(Some("my reason".to_string())) ); } #[test] fn smoke_add_from_branch() { let (_td, repo) = crate::test::repo_init(); let (wt_top, branch) = crate::test::worktrees_env_init(&repo); let wt_path = wt_top.path().join("test"); let mut opts = WorktreeAddOptions::new(); let reference = branch.into_reference(); opts.reference(Some(&reference)); let wt = repo .worktree("test-worktree", &wt_path, Some(&opts)) .unwrap(); assert_eq!(wt.name(), Some("test-worktree")); assert_eq!( wt.path().canonicalize().unwrap(), wt_path.canonicalize().unwrap() ); let status = wt.is_locked().unwrap(); assert_eq!(status, WorktreeLockStatus::Unlocked); } }