git2/
worktree.rs

1use crate::buf::Buf;
2use crate::reference::Reference;
3use crate::repo::Repository;
4use crate::util::{self, Binding};
5use crate::{raw, Error};
6use std::os::raw::c_int;
7use std::path::Path;
8use std::ptr;
9use std::str;
10use std::{marker, mem};
11
12/// An owned git worktree
13///
14/// This structure corresponds to a `git_worktree` in libgit2.
15//
16pub struct Worktree {
17    raw: *mut raw::git_worktree,
18}
19
20/// Options which can be used to configure how a worktree is initialized
21pub 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
27pub struct WorktreePruneOptions {
28    raw: raw::git_worktree_prune_options,
29}
30
31/// Lock Status of a worktree
32#[derive(PartialEq, Debug)]
33pub enum WorktreeLockStatus {
34    /// Worktree is Unlocked
35    Unlocked,
36    /// Worktree is locked with the optional message
37    Locked(Option<String>),
38}
39
40impl 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.
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
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.
71    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
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
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
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
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
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
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
144impl<'a> WorktreeAddOptions<'a> {
145    /// Creates a default set of add options.
146    ///
147    /// By default this will not lock the worktree
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
163    pub fn lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> {
164        self.raw.lock = enabled as c_int;
165        self
166    }
167
168    /// If enabled, this will checkout the existing branch matching the worktree name.
169    pub fn checkout_existing(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> {
170        self.raw.checkout_existing = enabled as c_int;
171        self
172    }
173
174    /// reference to use for the new worktree HEAD
175    pub fn reference(
176        &mut self,
177        reference: Option<&'a Reference<'_>>,
178    ) -> &mut WorktreeAddOptions<'a> {
179        self.raw.reference = if let Some(reference) = reference {
180            reference.raw()
181        } else {
182            ptr::null_mut()
183        };
184        self
185    }
186
187    /// Get a set of raw add options to be used with `git_worktree_add`
188    pub fn raw(&self) -> *const raw::git_worktree_add_options {
189        &self.raw
190    }
191}
192
193impl WorktreePruneOptions {
194    /// Creates a default set of pruning options
195    ///
196    /// By defaults this will prune only worktrees that are no longer valid
197    /// unlocked and not checked out
198    pub fn new() -> WorktreePruneOptions {
199        unsafe {
200            let mut raw = mem::zeroed();
201            assert_eq!(
202                raw::git_worktree_prune_options_init(
203                    &mut raw,
204                    raw::GIT_WORKTREE_PRUNE_OPTIONS_VERSION
205                ),
206                0
207            );
208            WorktreePruneOptions { raw }
209        }
210    }
211
212    /// Controls whether valid (still existing on the filesystem) worktrees
213    /// will be pruned
214    ///
215    /// Defaults to false
216    pub fn valid(&mut self, valid: bool) -> &mut WorktreePruneOptions {
217        self.flag(raw::GIT_WORKTREE_PRUNE_VALID, valid)
218    }
219
220    /// Controls whether locked worktrees will be pruned
221    ///
222    /// Defaults to false
223    pub fn locked(&mut self, locked: bool) -> &mut WorktreePruneOptions {
224        self.flag(raw::GIT_WORKTREE_PRUNE_LOCKED, locked)
225    }
226
227    /// Controls whether the actual working tree on the filesystem is recursively removed
228    ///
229    /// Defaults to false
230    pub fn working_tree(&mut self, working_tree: bool) -> &mut WorktreePruneOptions {
231        self.flag(raw::GIT_WORKTREE_PRUNE_WORKING_TREE, working_tree)
232    }
233
234    fn flag(&mut self, flag: raw::git_worktree_prune_t, on: bool) -> &mut WorktreePruneOptions {
235        if on {
236            self.raw.flags |= flag as u32;
237        } else {
238            self.raw.flags &= !(flag as u32);
239        }
240        self
241    }
242
243    /// Get a set of raw prune options to be used with `git_worktree_prune`
244    pub fn raw(&mut self) -> *mut raw::git_worktree_prune_options {
245        &mut self.raw
246    }
247}
248
249impl Binding for Worktree {
250    type Raw = *mut raw::git_worktree;
251    unsafe fn from_raw(ptr: *mut raw::git_worktree) -> Worktree {
252        Worktree { raw: ptr }
253    }
254    fn raw(&self) -> *mut raw::git_worktree {
255        self.raw
256    }
257}
258
259impl Drop for Worktree {
260    fn drop(&mut self) {
261        unsafe { raw::git_worktree_free(self.raw) }
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use crate::WorktreeAddOptions;
268    use crate::WorktreeLockStatus;
269
270    use tempfile::TempDir;
271
272    #[test]
273    fn smoke_add_no_ref() {
274        let (_td, repo) = crate::test::repo_init();
275
276        let wtdir = TempDir::new().unwrap();
277        let wt_path = wtdir.path().join("tree-no-ref-dir");
278        let opts = WorktreeAddOptions::new();
279
280        let wt = repo.worktree("tree-no-ref", &wt_path, Some(&opts)).unwrap();
281        assert_eq!(wt.name(), Some("tree-no-ref"));
282        assert_eq!(
283            wt.path().canonicalize().unwrap(),
284            wt_path.canonicalize().unwrap()
285        );
286        let status = wt.is_locked().unwrap();
287        assert_eq!(status, WorktreeLockStatus::Unlocked);
288    }
289
290    #[test]
291    fn smoke_add_locked() {
292        let (_td, repo) = crate::test::repo_init();
293
294        let wtdir = TempDir::new().unwrap();
295        let wt_path = wtdir.path().join("locked-tree");
296        let mut opts = WorktreeAddOptions::new();
297        opts.lock(true);
298
299        let wt = repo.worktree("locked-tree", &wt_path, Some(&opts)).unwrap();
300        // shouldn't be able to lock a worktree that was created locked
301        assert!(wt.lock(Some("my reason")).is_err());
302        assert_eq!(wt.name(), Some("locked-tree"));
303        assert_eq!(
304            wt.path().canonicalize().unwrap(),
305            wt_path.canonicalize().unwrap()
306        );
307        assert_eq!(wt.is_locked().unwrap(), WorktreeLockStatus::Locked(None));
308        assert!(wt.unlock().is_ok());
309        assert!(wt.lock(Some("my reason")).is_ok());
310        assert_eq!(
311            wt.is_locked().unwrap(),
312            WorktreeLockStatus::Locked(Some("my reason".to_string()))
313        );
314    }
315
316    #[test]
317    fn smoke_add_from_branch() {
318        let (_td, repo) = crate::test::repo_init();
319
320        let (wt_top, branch) = crate::test::worktrees_env_init(&repo);
321        let wt_path = wt_top.path().join("test");
322        let mut opts = WorktreeAddOptions::new();
323        let reference = branch.into_reference();
324        opts.reference(Some(&reference));
325
326        let wt = repo
327            .worktree("test-worktree", &wt_path, Some(&opts))
328            .unwrap();
329        assert_eq!(wt.name(), Some("test-worktree"));
330        assert_eq!(
331            wt.path().canonicalize().unwrap(),
332            wt_path.canonicalize().unwrap()
333        );
334        let status = wt.is_locked().unwrap();
335        assert_eq!(status, WorktreeLockStatus::Unlocked);
336    }
337}