1 use crate::buf::Buf; 2 use crate::reference::Reference; 3 use crate::repo::Repository; 4 use crate::util::{self, Binding}; 5 use crate::{raw, Error}; 6 use std::os::raw::c_int; 7 use std::path::Path; 8 use std::ptr; 9 use std::str; 10 use std::{marker, mem}; 11 12 /// An owned git worktree 13 /// 14 /// This structure corresponds to a `git_worktree` in libgit2. 15 // 16 pub struct Worktree { 17 raw: *mut raw::git_worktree, 18 } 19 20 /// Options which can be used to configure how a worktree is initialized 21 pub struct WorktreeAddOptions<'a> { 22 raw: raw::git_worktree_add_options, 23 _marker: marker::PhantomData<Reference<'a>>, 24 } 25 26 /// Options to configure how worktree pruning is performed 27 pub struct WorktreePruneOptions { 28 raw: raw::git_worktree_prune_options, 29 } 30 31 /// Lock Status of a worktree 32 #[derive(PartialEq, Debug)] 33 pub enum WorktreeLockStatus { 34 /// Worktree is Unlocked 35 Unlocked, 36 /// Worktree is locked with the optional message 37 Locked(Option<String>), 38 } 39 40 impl Worktree { 41 /// Open a worktree of a the repository 42 /// 43 /// If a repository is not the main tree but a worktree, this 44 /// function will look up the worktree inside the parent 45 /// repository and create a new `git_worktree` structure. open_from_repository(repo: &Repository) -> Result<Worktree, Error>46 pub fn open_from_repository(repo: &Repository) -> Result<Worktree, Error> { 47 let mut raw = ptr::null_mut(); 48 unsafe { 49 try_call!(raw::git_worktree_open_from_repository(&mut raw, repo.raw())); 50 Ok(Binding::from_raw(raw)) 51 } 52 } 53 54 /// Retrieves the name of the worktree 55 /// 56 /// This is the name that can be passed to repo::Repository::find_worktree 57 /// to reopen the worktree. This is also the name that would appear in the 58 /// list returned by repo::Repository::worktrees name(&self) -> Option<&str>59 pub fn name(&self) -> Option<&str> { 60 unsafe { 61 crate::opt_bytes(self, raw::git_worktree_name(self.raw)) 62 .and_then(|s| str::from_utf8(s).ok()) 63 } 64 } 65 66 /// Retrieves the path to the worktree 67 /// 68 /// This is the path to the top-level of the source and not the path to the 69 /// .git file within the worktree. This path can be passed to 70 /// repo::Repository::open. path(&self) -> &Path71 pub fn path(&self) -> &Path { 72 unsafe { 73 util::bytes2path(crate::opt_bytes(self, raw::git_worktree_path(self.raw)).unwrap()) 74 } 75 } 76 77 /// Validates the worktree 78 /// 79 /// This checks that it still exists on the 80 /// filesystem and that the metadata is correct validate(&self) -> Result<(), Error>81 pub fn validate(&self) -> Result<(), Error> { 82 unsafe { 83 try_call!(raw::git_worktree_validate(self.raw)); 84 } 85 Ok(()) 86 } 87 88 /// Locks the worktree lock(&self, reason: Option<&str>) -> Result<(), Error>89 pub fn lock(&self, reason: Option<&str>) -> Result<(), Error> { 90 let reason = crate::opt_cstr(reason)?; 91 unsafe { 92 try_call!(raw::git_worktree_lock(self.raw, reason)); 93 } 94 Ok(()) 95 } 96 97 /// Unlocks the worktree unlock(&self) -> Result<(), Error>98 pub fn unlock(&self) -> Result<(), Error> { 99 unsafe { 100 try_call!(raw::git_worktree_unlock(self.raw)); 101 } 102 Ok(()) 103 } 104 105 /// Checks if worktree is locked is_locked(&self) -> Result<WorktreeLockStatus, Error>106 pub fn is_locked(&self) -> Result<WorktreeLockStatus, Error> { 107 let buf = Buf::new(); 108 unsafe { 109 match try_call!(raw::git_worktree_is_locked(buf.raw(), self.raw)) { 110 0 => Ok(WorktreeLockStatus::Unlocked), 111 _ => { 112 let v = buf.to_vec(); 113 Ok(WorktreeLockStatus::Locked(match v.len() { 114 0 => None, 115 _ => Some(String::from_utf8(v).unwrap()), 116 })) 117 } 118 } 119 } 120 } 121 122 /// Prunes the worktree prune(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<(), Error>123 pub fn prune(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<(), Error> { 124 // When successful the worktree should be removed however the backing structure 125 // of the git_worktree should still be valid. 126 unsafe { 127 try_call!(raw::git_worktree_prune(self.raw, opts.map(|o| o.raw()))); 128 } 129 Ok(()) 130 } 131 132 /// Checks if the worktree is prunable is_prunable(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<bool, Error>133 pub fn is_prunable(&self, opts: Option<&mut WorktreePruneOptions>) -> Result<bool, Error> { 134 unsafe { 135 let rv = try_call!(raw::git_worktree_is_prunable( 136 self.raw, 137 opts.map(|o| o.raw()) 138 )); 139 Ok(rv != 0) 140 } 141 } 142 } 143 144 impl<'a> WorktreeAddOptions<'a> { 145 /// Creates a default set of add options. 146 /// 147 /// By default this will not lock the worktree new() -> WorktreeAddOptions<'a>148 pub fn new() -> WorktreeAddOptions<'a> { 149 unsafe { 150 let mut raw = mem::zeroed(); 151 assert_eq!( 152 raw::git_worktree_add_options_init(&mut raw, raw::GIT_WORKTREE_ADD_OPTIONS_VERSION), 153 0 154 ); 155 WorktreeAddOptions { 156 raw, 157 _marker: marker::PhantomData, 158 } 159 } 160 } 161 162 /// If enabled, this will cause the newly added worktree to be locked lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a>163 pub fn lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> { 164 self.raw.lock = enabled as c_int; 165 self 166 } 167 168 /// reference to use for the new worktree HEAD reference( &mut self, reference: Option<&'a Reference<'_>>, ) -> &mut WorktreeAddOptions<'a>169 pub fn reference( 170 &mut self, 171 reference: Option<&'a Reference<'_>>, 172 ) -> &mut WorktreeAddOptions<'a> { 173 self.raw.reference = if let Some(reference) = reference { 174 reference.raw() 175 } else { 176 ptr::null_mut() 177 }; 178 self 179 } 180 181 /// Get a set of raw add options to be used with `git_worktree_add` raw(&self) -> *const raw::git_worktree_add_options182 pub fn raw(&self) -> *const raw::git_worktree_add_options { 183 &self.raw 184 } 185 } 186 187 impl WorktreePruneOptions { 188 /// Creates a default set of pruning options 189 /// 190 /// By defaults this will prune only worktrees that are no longer valid 191 /// unlocked and not checked out new() -> WorktreePruneOptions192 pub fn new() -> WorktreePruneOptions { 193 unsafe { 194 let mut raw = mem::zeroed(); 195 assert_eq!( 196 raw::git_worktree_prune_options_init( 197 &mut raw, 198 raw::GIT_WORKTREE_PRUNE_OPTIONS_VERSION 199 ), 200 0 201 ); 202 WorktreePruneOptions { raw } 203 } 204 } 205 206 /// Controls whether valid (still existing on the filesystem) worktrees 207 /// will be pruned 208 /// 209 /// Defaults to false valid(&mut self, valid: bool) -> &mut WorktreePruneOptions210 pub fn valid(&mut self, valid: bool) -> &mut WorktreePruneOptions { 211 self.flag(raw::GIT_WORKTREE_PRUNE_VALID, valid) 212 } 213 214 /// Controls whether locked worktrees will be pruned 215 /// 216 /// Defaults to false locked(&mut self, locked: bool) -> &mut WorktreePruneOptions217 pub fn locked(&mut self, locked: bool) -> &mut WorktreePruneOptions { 218 self.flag(raw::GIT_WORKTREE_PRUNE_LOCKED, locked) 219 } 220 221 /// Controls whether the actual working tree on the fs is recursively removed 222 /// 223 /// Defaults to false working_tree(&mut self, working_tree: bool) -> &mut WorktreePruneOptions224 pub fn working_tree(&mut self, working_tree: bool) -> &mut WorktreePruneOptions { 225 self.flag(raw::GIT_WORKTREE_PRUNE_WORKING_TREE, working_tree) 226 } 227 flag(&mut self, flag: raw::git_worktree_prune_t, on: bool) -> &mut WorktreePruneOptions228 fn flag(&mut self, flag: raw::git_worktree_prune_t, on: bool) -> &mut WorktreePruneOptions { 229 if on { 230 self.raw.flags |= flag as u32; 231 } else { 232 self.raw.flags &= !(flag as u32); 233 } 234 self 235 } 236 237 /// Get a set of raw prune options to be used with `git_worktree_prune` raw(&mut self) -> *mut raw::git_worktree_prune_options238 pub fn raw(&mut self) -> *mut raw::git_worktree_prune_options { 239 &mut self.raw 240 } 241 } 242 243 impl Binding for Worktree { 244 type Raw = *mut raw::git_worktree; from_raw(ptr: *mut raw::git_worktree) -> Worktree245 unsafe fn from_raw(ptr: *mut raw::git_worktree) -> Worktree { 246 Worktree { raw: ptr } 247 } raw(&self) -> *mut raw::git_worktree248 fn raw(&self) -> *mut raw::git_worktree { 249 self.raw 250 } 251 } 252 253 impl Drop for Worktree { drop(&mut self)254 fn drop(&mut self) { 255 unsafe { raw::git_worktree_free(self.raw) } 256 } 257 } 258 259 #[cfg(test)] 260 mod tests { 261 use crate::WorktreeAddOptions; 262 use crate::WorktreeLockStatus; 263 264 use tempfile::TempDir; 265 266 #[test] smoke_add_no_ref()267 fn smoke_add_no_ref() { 268 let (_td, repo) = crate::test::repo_init(); 269 270 let wtdir = TempDir::new().unwrap(); 271 let wt_path = wtdir.path().join("tree-no-ref-dir"); 272 let opts = WorktreeAddOptions::new(); 273 274 let wt = repo.worktree("tree-no-ref", &wt_path, Some(&opts)).unwrap(); 275 assert_eq!(wt.name(), Some("tree-no-ref")); 276 assert_eq!( 277 wt.path().canonicalize().unwrap(), 278 wt_path.canonicalize().unwrap() 279 ); 280 let status = wt.is_locked().unwrap(); 281 assert_eq!(status, WorktreeLockStatus::Unlocked); 282 } 283 284 #[test] smoke_add_locked()285 fn smoke_add_locked() { 286 let (_td, repo) = crate::test::repo_init(); 287 288 let wtdir = TempDir::new().unwrap(); 289 let wt_path = wtdir.path().join("locked-tree"); 290 let mut opts = WorktreeAddOptions::new(); 291 opts.lock(true); 292 293 let wt = repo.worktree("locked-tree", &wt_path, Some(&opts)).unwrap(); 294 // shouldn't be able to lock a worktree that was created locked 295 assert!(wt.lock(Some("my reason")).is_err()); 296 assert_eq!(wt.name(), Some("locked-tree")); 297 assert_eq!( 298 wt.path().canonicalize().unwrap(), 299 wt_path.canonicalize().unwrap() 300 ); 301 assert_eq!(wt.is_locked().unwrap(), WorktreeLockStatus::Locked(None)); 302 assert!(wt.unlock().is_ok()); 303 assert!(wt.lock(Some("my reason")).is_ok()); 304 assert_eq!( 305 wt.is_locked().unwrap(), 306 WorktreeLockStatus::Locked(Some("my reason".to_string())) 307 ); 308 } 309 310 #[test] smoke_add_from_branch()311 fn smoke_add_from_branch() { 312 let (_td, repo) = crate::test::repo_init(); 313 314 let (wt_top, branch) = crate::test::worktrees_env_init(&repo); 315 let wt_path = wt_top.path().join("test"); 316 let mut opts = WorktreeAddOptions::new(); 317 let reference = branch.into_reference(); 318 opts.reference(Some(&reference)); 319 320 let wt = repo 321 .worktree("test-worktree", &wt_path, Some(&opts)) 322 .unwrap(); 323 assert_eq!(wt.name(), Some("test-worktree")); 324 assert_eq!( 325 wt.path().canonicalize().unwrap(), 326 wt_path.canonicalize().unwrap() 327 ); 328 let status = wt.is_locked().unwrap(); 329 assert_eq!(status, WorktreeLockStatus::Unlocked); 330 } 331 } 332