diff --git a/Cargo.toml b/Cargo.toml index a5c2a82ccc..2e2375a5b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ structopt = "0.3" time = "0.1.39" tempfile = "3.1.0" thread-id = "3.3.0" # remove when we work with minimal-versions without it +paste = "0.1.12" [features] unstable = [] diff --git a/libgit2-sys/lib.rs b/libgit2-sys/lib.rs index 5bf894d07e..25a6adb380 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -1823,6 +1823,34 @@ git_enum! { } } +#[repr(C)] +pub struct git_worktree_add_options { + pub version: c_uint, + pub lock: c_int, + pub reference: *mut git_reference, +} + +pub const GIT_WORKTREE_ADD_OPTIONS_VERSION: c_uint = 1; + +git_enum! { + pub enum git_worktree_prune_t { + /* Prune working tree even if working tree is valid */ + GIT_WORKTREE_PRUNE_VALID = 1 << 0, + /* Prune working tree even if it is locked */ + GIT_WORKTREE_PRUNE_LOCKED = 1 << 1, + /* Prune checked out working tree */ + GIT_WORKTREE_PRUNE_WORKING_TREE = 1 << 2, + } +} + +#[repr(C)] +pub struct git_worktree_prune_options { + pub version: c_uint, + pub flags: u32, +} + +pub const GIT_WORKTREE_PRUNE_OPTIONS_VERSION: c_uint = 1; + extern "C" { // threads pub fn git_libgit2_init() -> c_int; @@ -3780,6 +3808,48 @@ extern "C" { ) -> c_int; pub fn git_libgit2_opts(option: c_int, ...) -> c_int; + + // Worktrees + pub fn git_worktree_list(out: *mut git_strarray, repo: *mut git_repository) -> c_int; + pub fn git_worktree_lookup( + out: *mut *mut git_worktree, + repo: *mut git_repository, + name: *const c_char, + ) -> c_int; + pub fn git_worktree_open_from_repository( + out: *mut *mut git_worktree, + repo: *mut git_repository, + ) -> c_int; + pub fn git_worktree_free(wt: *mut git_worktree); + pub fn git_worktree_validate(wt: *const git_worktree) -> c_int; + pub fn git_worktree_add_options_init( + opts: *mut git_worktree_add_options, + version: c_uint, + ) -> c_int; + pub fn git_worktree_add( + out: *mut *mut git_worktree, + repo: *mut git_repository, + name: *const c_char, + path: *const c_char, + opts: *const git_worktree_add_options, + ) -> c_int; + pub fn git_worktree_lock(wt: *mut git_worktree, reason: *const c_char) -> c_int; + pub fn git_worktree_unlock(wt: *mut git_worktree) -> c_int; + pub fn git_worktree_is_locked(reason: *mut git_buf, wt: *const git_worktree) -> c_int; + pub fn git_worktree_name(wt: *const git_worktree) -> *const c_char; + pub fn git_worktree_path(wt: *const git_worktree) -> *const c_char; + pub fn git_worktree_prune_options_init( + opts: *mut git_worktree_prune_options, + version: c_uint, + ) -> c_int; + pub fn git_worktree_is_prunable( + wt: *mut git_worktree, + opts: *mut git_worktree_prune_options, + ) -> c_int; + pub fn git_worktree_prune( + wt: *mut git_worktree, + opts: *mut git_worktree_prune_options, + ) -> c_int; } pub fn init() { diff --git a/src/blame.rs b/src/blame.rs index 5c582458df..b45dc258c6 100644 --- a/src/blame.rs +++ b/src/blame.rs @@ -295,7 +295,7 @@ mod tests { let (_td, repo) = crate::test::repo_init(); let mut index = repo.index().unwrap(); - let root = repo.path().parent().unwrap(); + let root = repo.workdir().unwrap(); fs::create_dir(&root.join("foo")).unwrap(); File::create(&root.join("foo/bar")).unwrap(); index.add_path(Path::new("foo/bar")).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index ace4009230..cb2e871cca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -131,6 +131,7 @@ pub use crate::time::{IndexTime, Time}; pub use crate::tree::{Tree, TreeEntry, TreeIter, TreeWalkMode, TreeWalkResult}; pub use crate::treebuilder::TreeBuilder; pub use crate::util::IntoCString; +pub use crate::worktree::{Worktree, WorktreeAddOptions, WorktreeLockStatus, WorktreePruneOptions}; // Create a convinience method on bitflag struct which checks the given flag macro_rules! is_bit_set { @@ -686,6 +687,7 @@ mod tagforeach; mod time; mod tree; mod treebuilder; +mod worktree; fn init() { static INIT: Once = Once::new(); diff --git a/src/repo.rs b/src/repo.rs index 4f53ccb3fc..113112e8a2 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -16,6 +16,7 @@ use crate::stash::{stash_cb, StashApplyOptions, StashCbData}; use crate::string_array::StringArray; use crate::tagforeach::{tag_foreach_cb, TagForeachCB, TagForeachData}; use crate::util::{self, path_to_repo_path, Binding}; +use crate::worktree::{Worktree, WorktreeAddOptions}; use crate::CherrypickOptions; use crate::RevertOptions; use crate::{ @@ -162,6 +163,18 @@ impl Repository { } } + /// Attempt to open an already-existing repository from a worktree. + pub fn open_from_worktree(worktree: &Worktree) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_from_worktree( + &mut ret, + worktree.raw() + )); + Ok(Binding::from_raw(ret)) + } + } + /// Attempt to open an already-existing repository at or above `path` /// /// This starts at `path` and looks up the filesystem hierarchy @@ -2791,6 +2804,53 @@ impl Repository { Ok(Binding::from_raw(ret)) } } + + /// Lists all the worktrees for the repository + pub fn worktrees(&self) -> Result { + let mut arr = raw::git_strarray { + strings: ptr::null_mut(), + count: 0, + }; + unsafe { + try_call!(raw::git_worktree_list(&mut arr, self.raw)); + Ok(Binding::from_raw(arr)) + } + } + + /// Opens a worktree by name for the given repository + /// + /// This can open any worktree that the worktrees method returns. + pub fn find_worktree(&self, name: &str) -> Result { + let mut raw = ptr::null_mut(); + let raw_name = CString::new(name)?; + unsafe { + try_call!(raw::git_worktree_lookup(&mut raw, self.raw, raw_name)); + Ok(Binding::from_raw(raw)) + } + } + + /// Creates a new worktree for the repository + pub fn worktree<'a>( + &'a self, + name: &str, + path: &Path, + opts: Option<&WorktreeAddOptions<'a>>, + ) -> Result { + let mut raw = ptr::null_mut(); + let raw_name = CString::new(name)?; + let raw_path = path.into_c_string()?; + + unsafe { + try_call!(raw::git_worktree_add( + &mut raw, + self.raw, + raw_name, + raw_path, + opts.map(|o| o.raw()) + )); + Ok(Binding::from_raw(raw)) + } + } } impl Binding for Repository { diff --git a/src/test.rs b/src/test.rs index f21690e355..89a13beafe 100644 --- a/src/test.rs +++ b/src/test.rs @@ -6,7 +6,7 @@ use std::ptr; use tempfile::TempDir; use url::Url; -use crate::{Oid, Repository, RepositoryInitOptions}; +use crate::{Branch, Oid, Repository, RepositoryInitOptions}; macro_rules! t { ($e:expr) => { @@ -56,6 +56,14 @@ pub fn path2url(path: &Path) -> String { Url::from_file_path(path).unwrap().to_string() } +pub fn worktrees_env_init(repo: &Repository) -> (TempDir, Branch<'_>) { + let oid = repo.head().unwrap().target().unwrap(); + let commit = repo.find_commit(oid).unwrap(); + let branch = repo.branch("wt-branch", &commit, true).unwrap(); + let wtdir = TempDir::new().unwrap(); + (wtdir, branch) +} + #[cfg(windows)] pub fn realpath(original: &Path) -> io::Result { Ok(original.to_path_buf()) diff --git a/src/worktree.rs b/src/worktree.rs new file mode 100644 index 0000000000..3cb75c6edf --- /dev/null +++ b/src/worktree.rs @@ -0,0 +1,331 @@ +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 fs 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); + } +} diff --git a/systest/build.rs b/systest/build.rs index 23a57bdc1e..eba93f3090 100644 --- a/systest/build.rs +++ b/systest/build.rs @@ -22,7 +22,9 @@ fn main() { }); cfg.skip_field(|struct_, f| { // this field is marked as const which ctest complains about - struct_ == "git_rebase_operation" && f == "id" + (struct_ == "git_rebase_operation" && f == "id") || + // the real name of this field is ref but that is a reserved keyword + (struct_ == "git_worktree_add_options" && f == "reference") }); cfg.skip_signededness(|s| { match s {