diff --git a/Cargo.toml b/Cargo.toml index 61880f9363..160afd97b0 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 ae2d534a37..e81448ccd6 100644 --- a/libgit2-sys/lib.rs +++ b/libgit2-sys/lib.rs @@ -1758,6 +1758,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: u32 = 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: u32 = 1; + extern "C" { // threads pub fn git_libgit2_init() -> c_int; @@ -3645,6 +3673,48 @@ extern "C" { location: git_apply_location_t, options: *const git_apply_options, ) -> 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..94a500295d 100644 --- a/src/blame.rs +++ b/src/blame.rs @@ -290,9 +290,7 @@ mod tests { use std::fs::{self, File}; use std::path::Path; - #[test] - fn smoke() { - let (_td, repo) = crate::test::repo_init(); + repo_test!(smoke, Typical, TypicalWorktree, BareWorktree { let mut index = repo.index().unwrap(); let root = repo.path().parent().unwrap(); @@ -322,5 +320,5 @@ mod tests { assert_eq!(hunk.path(), Some(Path::new("foo/bar"))); assert_eq!(hunk.lines_in_hunk(), 0); assert!(!hunk.is_boundary()) - } + }); } diff --git a/src/branch.rs b/src/branch.rs index c33a18370e..18cbdca86a 100644 --- a/src/branch.rs +++ b/src/branch.rs @@ -153,8 +153,7 @@ impl<'repo> Drop for Branches<'repo> { mod tests { use crate::BranchType; - #[test] - fn smoke() { + repo_test!(smoke, Typical, TypicalWorktree, BareWorktree { let (_td, repo) = crate::test::repo_init(); let head = repo.head().unwrap(); let target = head.target().unwrap(); @@ -174,5 +173,5 @@ mod tests { b1.set_upstream(None).unwrap(); b1.delete().unwrap(); - } + }); } diff --git a/src/call.rs b/src/call.rs index f1ade4a8f3..78f3df6a09 100644 --- a/src/call.rs +++ b/src/call.rs @@ -93,6 +93,12 @@ mod impls { } } + impl Convert<*mut libc::c_char> for CString { + fn convert(&self) -> *mut libc::c_char { + self.as_ptr() as *mut libc::c_char + } + } + impl> Convert<*const T> for Option { fn convert(&self) -> *const T { self.as_ref().map(|s| s.convert()).unwrap_or(ptr::null()) diff --git a/src/lib.rs b/src/lib.rs index 0513fcc03b..8c70137ab6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,6 +129,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 { @@ -666,6 +667,7 @@ mod tag; 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 88d5f8c044..53cdc45a28 100644 --- a/src/repo.rs +++ b/src/repo.rs @@ -15,6 +15,7 @@ use crate::oid_array::OidArray; use crate::stash::{stash_cb, StashApplyOptions, StashCbData}; use crate::string_array::StringArray; use crate::util::{self, path_to_repo_path, Binding}; +use crate::worktree::{Worktree, WorktreeAddOptions}; use crate::CherrypickOptions; use crate::{ init, raw, AttrCheckFlags, Buf, Error, Object, Remote, RepositoryOpenFlags, RepositoryState, @@ -2700,6 +2701,66 @@ impl Repository { Ok(()) } } + + /// Lists all the worktrees for the repository + pub fn worktrees(&self) -> Result { + let mut arr = raw::git_strarray { + strings: 0 as *mut *mut c_char, + 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 worktree_lookup(&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)) + } + } + + /// 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 worktree_open_from_repository(&self) -> Result { + let mut raw = ptr::null_mut(); + unsafe { + try_call!(raw::git_worktree_open_from_repository(&mut raw, self.raw)); + Ok(Binding::from_raw(raw)) + } + } + + /// Creates a new worktree for the repository + pub fn worktree_add( + &self, + name: &str, + path: &Path, + opts: &WorktreeAddOptions<'_>, + ) -> 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.raw() + )); + Ok(Binding::from_raw(raw)) + } + } } impl Binding for Repository { diff --git a/src/test.rs b/src/test.rs index 149f4946c5..6ea4c50cd3 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}; +use crate::{Branch, Oid, Repository}; macro_rules! t { ($e:expr) => { @@ -17,7 +17,21 @@ macro_rules! t { }; } -pub fn repo_init() -> (TempDir, Repository) { +// `repo_test! will +macro_rules! repo_test { + ($test_name:ident, $($repo_type:ident),+ $test_body:block) => { + paste::item! { + $(#[test] + fn [<$test_name _ $repo_type:snake>]() { + #[allow(unused_variables)] + let (td, repo) = $crate::test::repo_init2($crate::test::RepoType::$repo_type); + $test_body + })+ + } + } +} + +pub fn repo_init_typical() -> (TempDir, Repository) { let td = TempDir::new().unwrap(); let repo = Repository::init(td.path()).unwrap(); { @@ -35,6 +49,31 @@ pub fn repo_init() -> (TempDir, Repository) { (td, repo) } +pub fn repo_init_bare() -> (TempDir, Repository) { + panic!("unimplemented") +} + +pub fn repo_init_bare_worktree() -> (TempDir, Repository) { + panic!("unimplemented") +} + +pub fn repo_init_typical_worktree() -> (TempDir, Repository) { + panic!("unimplemented") +} + +pub fn repo_init() -> (TempDir, Repository) { + repo_init_typical() +} + +pub fn repo_init2(repo_type: RepoType) -> (TempDir, Repository) { + match repo_type { + RepoType::Typical => repo_init_typical(), + RepoType::Bare => repo_init_bare(), + RepoType::BareWorktree => repo_init_bare_worktree(), + RepoType::TypicalWorktree => repo_init_typical_worktree(), + } +} + pub fn commit(repo: &Repository) -> (Oid, Oid) { let mut index = t!(repo.index()); let root = repo.path().parent().unwrap(); @@ -54,6 +93,22 @@ 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) +} + +#[derive(Debug, Clone, Copy)] +pub enum RepoType { + Typical, + TypicalWorktree, + Bare, + BareWorktree, +} + #[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..262e6a18c7 --- /dev/null +++ b/src/worktree.rs @@ -0,0 +1,319 @@ +use crate::buf::Buf; +use crate::reference::Reference; +use crate::repo::Repository; +use crate::util::{self, Binding}; +use crate::{call, raw, Error}; +use std::mem; +use std::path::Path; +use std::ptr; +use std::str; + +/// An owned git worktree +/// +/// This structure corresponds to a `git_worktree` in libgit2. +// +pub struct Worktree { + raw: *mut raw::git_worktree, +} + +// It is the current belief that a `Worktree` can be sent among threads, or +// even shared among threads in a mutex +unsafe impl Send for Worktree {} + +/// Options which can be used to configure how a repository is initialized +pub struct WorktreeAddOptions<'a> { + lock: i32, + reference: Option>, +} + +/// Options to configure how worktree pruning is performed +pub struct WorktreePruneOptions { + flags: u32, +} + +/// 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 { + /// Retrieves the name of the worktree + /// + /// This is the name that can be passed to repo::Repository::worktree_lookup + /// 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 { + call::c_try(raw::git_worktree_validate(call::convert(&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 { + call::c_try(raw::git_worktree_unlock(call::convert(&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), + _ => { + println!("Buf: {}", buf.as_str().unwrap()); + let v = buf.to_vec(); + println!("Length of v: {}", v.len()); + Ok(WorktreeLockStatus::Locked(match v.len() { + 0 => None, + _ => String::from_utf8(v).ok(), + })) + } + } + } + } + + /// Prunes the worktree + pub fn prune(&self, opts: 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, &mut opts.raw())); + } + Ok(()) + } + + /// Checks if the worktree is prunable + pub fn is_prunable(&self, opts: WorktreePruneOptions) -> bool { + unsafe { + if call!(raw::git_worktree_is_prunable(self.raw, &mut opts.raw())) <= 0 { + false + } else { + true + } + } + } + + /// Opens the repository from the worktree + pub fn open_repository(&self) -> Result { + let mut ret = ptr::null_mut(); + unsafe { + try_call!(raw::git_repository_open_from_worktree(&mut ret, self.raw)); + Ok(Binding::from_raw(ret)) + } + } +} + +impl<'a> WorktreeAddOptions<'a> { + /// Creates a default set of add options. + /// + /// By default this will not lock the worktree + pub fn new(reference: Option>) -> WorktreeAddOptions<'a> { + WorktreeAddOptions { + lock: 0, + reference: reference, + } + } + + /// If enabled, this will cause the newly added worktree to be locked + pub fn lock(&mut self, enabled: bool) -> &mut WorktreeAddOptions<'a> { + self.lock = match enabled { + true => 1, + false => 0, + }; + self + } + + /// Creates a set of raw add options to be used with `git_worktree_add` + /// + /// This method is unsafe as the returned value may have pointers to the + /// interior of this structure + pub unsafe fn raw(&self) -> raw::git_worktree_add_options { + let mut opts = mem::zeroed(); + assert_eq!( + raw::git_worktree_add_options_init(&mut opts, raw::GIT_WORKTREE_ADD_OPTIONS_VERSION), + 0 + ); + + opts.lock = self.lock; + opts.reference = if let Some(ref gref) = self.reference { + gref.raw() + } else { + ptr::null_mut() + }; + + opts + } +} + +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 { + WorktreePruneOptions { flags: 0 } + } + + /// 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.flags |= flag as u32; + } else { + self.flags &= !(flag as u32); + } + self + } + /// Creates a set of raw prune options to be used with `git_worktree_prune` + /// + /// This method is unsafe as the returned value may have pointers to the + /// interior of this structure + pub unsafe fn raw(&self) -> raw::git_worktree_prune_options { + let mut opts = mem::zeroed(); + assert_eq!( + raw::git_worktree_prune_options_init( + &mut opts, + raw::GIT_WORKTREE_PRUNE_OPTIONS_VERSION + ), + 0 + ); + + opts.flags = self.flags; + opts + } +} + +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; + + repo_test!(smoke_add_no_ref, Typical, Bare { + let wtdir = TempDir::new().unwrap(); + let wt_path = wtdir.path().join("tree-no-ref-dir"); + let opts = WorktreeAddOptions::new(None); + + let wt = repo.worktree_add("tree-no-ref", &wt_path, &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); + }); + + repo_test!(smoke_add_locked, Typical, Bare { + let wtdir = TempDir::new().unwrap(); + let wt_path = wtdir.path().join("locked-tree"); + let mut opts = WorktreeAddOptions::new(None); + opts.lock(true); + + let wt = repo.worktree_add("locked-tree", &wt_path, &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())) + ); + }); + + repo_test!(smoke_add_from_branch, Typical, Bare { + let (wt_top, branch) = crate::test::worktrees_env_init(&repo); + let wt_path = wt_top.path().join("test"); + let opts = WorktreeAddOptions::new(Some(branch.into_reference())); + + let wt = repo.worktree_add("test-worktree", &wt_path, &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..64942afc64 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 { @@ -41,5 +43,6 @@ fn main() { cfg.skip_roundtrip(|t| t == "git_clone_options" || t == "git_submodule_update_options"); cfg.skip_type(|t| t == "__enum_ty"); + cfg.skip_type(|t| t == "git_transfer_progress"); cfg.generate("../libgit2-sys/lib.rs", "all.rs"); }