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