| //! Builder-pattern objects for configuration various git operations. |
| |
| use libc::{c_char, c_int, c_uint, c_void, size_t}; |
| use std::ffi::{CStr, CString}; |
| use std::mem; |
| use std::path::Path; |
| use std::ptr; |
| |
| use crate::util::{self, Binding}; |
| use crate::{panic, raw, Error, FetchOptions, IntoCString, Oid, Repository, Tree}; |
| use crate::{CheckoutNotificationType, DiffFile, FileMode, Remote}; |
| |
| /// A builder struct which is used to build configuration for cloning a new git |
| /// repository. |
| /// |
| /// # Example |
| /// |
| /// Cloning using SSH: |
| /// |
| /// ```no_run |
| /// use git2::{Cred, Error, RemoteCallbacks}; |
| /// use std::env; |
| /// use std::path::Path; |
| /// |
| /// // Prepare callbacks. |
| /// let mut callbacks = RemoteCallbacks::new(); |
| /// callbacks.credentials(|_url, username_from_url, _allowed_types| { |
| /// Cred::ssh_key( |
| /// username_from_url.unwrap(), |
| /// None, |
| /// std::path::Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())), |
| /// None, |
| /// ) |
| /// }); |
| /// |
| /// // Prepare fetch options. |
| /// let mut fo = git2::FetchOptions::new(); |
| /// fo.remote_callbacks(callbacks); |
| /// |
| /// // Prepare builder. |
| /// let mut builder = git2::build::RepoBuilder::new(); |
| /// builder.fetch_options(fo); |
| /// |
| /// // Clone the project. |
| /// builder.clone( |
| /// "[email protected]:rust-lang/git2-rs.git", |
| /// Path::new("/tmp/git2-rs"), |
| /// ); |
| /// ``` |
| pub struct RepoBuilder<'cb> { |
| bare: bool, |
| branch: Option<CString>, |
| local: bool, |
| hardlinks: bool, |
| checkout: Option<CheckoutBuilder<'cb>>, |
| fetch_opts: Option<FetchOptions<'cb>>, |
| clone_local: Option<CloneLocal>, |
| remote_create: Option<Box<RemoteCreate<'cb>>>, |
| } |
| |
| /// Type of callback passed to `RepoBuilder::remote_create`. |
| /// |
| /// The second and third arguments are the remote's name and the remote's url. |
| pub type RemoteCreate<'cb> = |
| dyn for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb; |
| |
| /// A builder struct for git tree updates, for use with `git_tree_create_updated`. |
| pub struct TreeUpdateBuilder { |
| updates: Vec<raw::git_tree_update>, |
| paths: Vec<CString>, |
| } |
| |
| /// A builder struct for configuring checkouts of a repository. |
| pub struct CheckoutBuilder<'cb> { |
| their_label: Option<CString>, |
| our_label: Option<CString>, |
| ancestor_label: Option<CString>, |
| target_dir: Option<CString>, |
| paths: Vec<CString>, |
| path_ptrs: Vec<*const c_char>, |
| file_perm: Option<i32>, |
| dir_perm: Option<i32>, |
| disable_filters: bool, |
| checkout_opts: u32, |
| progress: Option<Box<Progress<'cb>>>, |
| notify: Option<Box<Notify<'cb>>>, |
| notify_flags: CheckoutNotificationType, |
| } |
| |
| /// Checkout progress notification callback. |
| /// |
| /// The first argument is the path for the notification, the next is the numver |
| /// of completed steps so far, and the final is the total number of steps. |
| pub type Progress<'a> = dyn FnMut(Option<&Path>, usize, usize) + 'a; |
| |
| /// Checkout notifications callback. |
| /// |
| /// The first argument is the notification type, the next is the path for the |
| /// the notification, followed by the baseline diff, target diff, and workdir diff. |
| /// |
| /// The callback must return a bool specifying whether the checkout should |
| /// continue. |
| pub type Notify<'a> = dyn FnMut( |
| CheckoutNotificationType, |
| Option<&Path>, |
| Option<DiffFile<'_>>, |
| Option<DiffFile<'_>>, |
| Option<DiffFile<'_>>, |
| ) -> bool |
| + 'a; |
| |
| impl<'cb> Default for RepoBuilder<'cb> { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| /// Options that can be passed to `RepoBuilder::clone_local`. |
| #[derive(Clone, Copy)] |
| pub enum CloneLocal { |
| /// Auto-detect (default) |
| /// |
| /// Here libgit2 will bypass the git-aware transport for local paths, but |
| /// use a normal fetch for `file://` urls. |
| Auto = raw::GIT_CLONE_LOCAL_AUTO as isize, |
| |
| /// Bypass the git-aware transport even for `file://` urls. |
| Local = raw::GIT_CLONE_LOCAL as isize, |
| |
| /// Never bypass the git-aware transport |
| None = raw::GIT_CLONE_NO_LOCAL as isize, |
| |
| /// Bypass the git-aware transport, but don't try to use hardlinks. |
| NoLinks = raw::GIT_CLONE_LOCAL_NO_LINKS as isize, |
| |
| #[doc(hidden)] |
| __Nonexhaustive = 0xff, |
| } |
| |
| impl<'cb> RepoBuilder<'cb> { |
| /// Creates a new repository builder with all of the default configuration. |
| /// |
| /// When ready, the `clone()` method can be used to clone a new repository |
| /// using this configuration. |
| pub fn new() -> RepoBuilder<'cb> { |
| crate::init(); |
| RepoBuilder { |
| bare: false, |
| branch: None, |
| local: true, |
| clone_local: None, |
| hardlinks: true, |
| checkout: None, |
| fetch_opts: None, |
| remote_create: None, |
| } |
| } |
| |
| /// Indicate whether the repository will be cloned as a bare repository or |
| /// not. |
| pub fn bare(&mut self, bare: bool) -> &mut RepoBuilder<'cb> { |
| self.bare = bare; |
| self |
| } |
| |
| /// Specify the name of the branch to check out after the clone. |
| /// |
| /// If not specified, the remote's default branch will be used. |
| pub fn branch(&mut self, branch: &str) -> &mut RepoBuilder<'cb> { |
| self.branch = Some(CString::new(branch).unwrap()); |
| self |
| } |
| |
| /// Configures options for bypassing the git-aware transport on clone. |
| /// |
| /// Bypassing it means that instead of a fetch libgit2 will copy the object |
| /// database directory instead of figuring out what it needs, which is |
| /// faster. If possible, it will hardlink the files to save space. |
| pub fn clone_local(&mut self, clone_local: CloneLocal) -> &mut RepoBuilder<'cb> { |
| self.clone_local = Some(clone_local); |
| self |
| } |
| |
| /// Set the flag for bypassing the git aware transport mechanism for local |
| /// paths. |
| /// |
| /// If `true`, the git-aware transport will be bypassed for local paths. If |
| /// `false`, the git-aware transport will not be bypassed. |
| #[deprecated(note = "use `clone_local` instead")] |
| #[doc(hidden)] |
| pub fn local(&mut self, local: bool) -> &mut RepoBuilder<'cb> { |
| self.local = local; |
| self |
| } |
| |
| /// Set the flag for whether hardlinks are used when using a local git-aware |
| /// transport mechanism. |
| #[deprecated(note = "use `clone_local` instead")] |
| #[doc(hidden)] |
| pub fn hardlinks(&mut self, links: bool) -> &mut RepoBuilder<'cb> { |
| self.hardlinks = links; |
| self |
| } |
| |
| /// Configure the checkout which will be performed by consuming a checkout |
| /// builder. |
| pub fn with_checkout(&mut self, checkout: CheckoutBuilder<'cb>) -> &mut RepoBuilder<'cb> { |
| self.checkout = Some(checkout); |
| self |
| } |
| |
| /// Options which control the fetch, including callbacks. |
| /// |
| /// The callbacks are used for reporting fetch progress, and for acquiring |
| /// credentials in the event they are needed. |
| pub fn fetch_options(&mut self, fetch_opts: FetchOptions<'cb>) -> &mut RepoBuilder<'cb> { |
| self.fetch_opts = Some(fetch_opts); |
| self |
| } |
| |
| /// Configures a callback used to create the git remote, prior to its being |
| /// used to perform the clone operation. |
| pub fn remote_create<F>(&mut self, f: F) -> &mut RepoBuilder<'cb> |
| where |
| F: for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb, |
| { |
| self.remote_create = Some(Box::new(f)); |
| self |
| } |
| |
| /// Clone a remote repository. |
| /// |
| /// This will use the options configured so far to clone the specified url |
| /// into the specified local path. |
| pub fn clone(&mut self, url: &str, into: &Path) -> Result<Repository, Error> { |
| let mut opts: raw::git_clone_options = unsafe { mem::zeroed() }; |
| unsafe { |
| try_call!(raw::git_clone_init_options( |
| &mut opts, |
| raw::GIT_CLONE_OPTIONS_VERSION |
| )); |
| } |
| opts.bare = self.bare as c_int; |
| opts.checkout_branch = self |
| .branch |
| .as_ref() |
| .map(|s| s.as_ptr()) |
| .unwrap_or(ptr::null()); |
| |
| if let Some(ref local) = self.clone_local { |
| opts.local = *local as raw::git_clone_local_t; |
| } else { |
| opts.local = match (self.local, self.hardlinks) { |
| (true, false) => raw::GIT_CLONE_LOCAL_NO_LINKS, |
| (false, _) => raw::GIT_CLONE_NO_LOCAL, |
| (true, _) => raw::GIT_CLONE_LOCAL_AUTO, |
| }; |
| } |
| |
| if let Some(ref mut cbs) = self.fetch_opts { |
| opts.fetch_opts = cbs.raw(); |
| } |
| |
| if let Some(ref mut c) = self.checkout { |
| unsafe { |
| c.configure(&mut opts.checkout_opts); |
| } |
| } |
| |
| if let Some(ref mut callback) = self.remote_create { |
| opts.remote_cb = Some(remote_create_cb); |
| opts.remote_cb_payload = callback as *mut _ as *mut _; |
| } |
| |
| let url = CString::new(url)?; |
| // Normal file path OK (does not need Windows conversion). |
| let into = into.into_c_string()?; |
| let mut raw = ptr::null_mut(); |
| unsafe { |
| try_call!(raw::git_clone(&mut raw, url, into, &opts)); |
| Ok(Binding::from_raw(raw)) |
| } |
| } |
| } |
| |
| extern "C" fn remote_create_cb( |
| out: *mut *mut raw::git_remote, |
| repo: *mut raw::git_repository, |
| name: *const c_char, |
| url: *const c_char, |
| payload: *mut c_void, |
| ) -> c_int { |
| unsafe { |
| let repo = Repository::from_raw(repo); |
| let code = panic::wrap(|| { |
| let name = CStr::from_ptr(name).to_str().unwrap(); |
| let url = CStr::from_ptr(url).to_str().unwrap(); |
| let f = payload as *mut Box<RemoteCreate<'_>>; |
| match (*f)(&repo, name, url) { |
| Ok(remote) => { |
| *out = crate::remote::remote_into_raw(remote); |
| 0 |
| } |
| Err(e) => e.raw_code(), |
| } |
| }); |
| mem::forget(repo); |
| code.unwrap_or(-1) |
| } |
| } |
| |
| impl<'cb> Default for CheckoutBuilder<'cb> { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl<'cb> CheckoutBuilder<'cb> { |
| /// Creates a new builder for checkouts with all of its default |
| /// configuration. |
| pub fn new() -> CheckoutBuilder<'cb> { |
| crate::init(); |
| CheckoutBuilder { |
| disable_filters: false, |
| dir_perm: None, |
| file_perm: None, |
| path_ptrs: Vec::new(), |
| paths: Vec::new(), |
| target_dir: None, |
| ancestor_label: None, |
| our_label: None, |
| their_label: None, |
| checkout_opts: raw::GIT_CHECKOUT_SAFE as u32, |
| progress: None, |
| notify: None, |
| notify_flags: CheckoutNotificationType::empty(), |
| } |
| } |
| |
| /// Indicate that this checkout should perform a dry run by checking for |
| /// conflicts but not make any actual changes. |
| pub fn dry_run(&mut self) -> &mut CheckoutBuilder<'cb> { |
| self.checkout_opts &= !((1 << 4) - 1); |
| self.checkout_opts |= raw::GIT_CHECKOUT_NONE as u32; |
| self |
| } |
| |
| /// Take any action necessary to get the working directory to match the |
| /// target including potentially discarding modified files. |
| pub fn force(&mut self) -> &mut CheckoutBuilder<'cb> { |
| self.checkout_opts &= !((1 << 4) - 1); |
| self.checkout_opts |= raw::GIT_CHECKOUT_FORCE as u32; |
| self |
| } |
| |
| /// Indicate that the checkout should be performed safely, allowing new |
| /// files to be created but not overwriting extisting files or changes. |
| /// |
| /// This is the default. |
| pub fn safe(&mut self) -> &mut CheckoutBuilder<'cb> { |
| self.checkout_opts &= !((1 << 4) - 1); |
| self.checkout_opts |= raw::GIT_CHECKOUT_SAFE as u32; |
| self |
| } |
| |
| fn flag(&mut self, bit: raw::git_checkout_strategy_t, on: bool) -> &mut CheckoutBuilder<'cb> { |
| if on { |
| self.checkout_opts |= bit as u32; |
| } else { |
| self.checkout_opts &= !(bit as u32); |
| } |
| self |
| } |
| |
| /// In safe mode, create files that don't exist. |
| /// |
| /// Defaults to false. |
| pub fn recreate_missing(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_RECREATE_MISSING, allow) |
| } |
| |
| /// In safe mode, apply safe file updates even when there are conflicts |
| /// instead of canceling the checkout. |
| /// |
| /// Defaults to false. |
| pub fn allow_conflicts(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_ALLOW_CONFLICTS, allow) |
| } |
| |
| /// Remove untracked files from the working dir. |
| /// |
| /// Defaults to false. |
| pub fn remove_untracked(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_REMOVE_UNTRACKED, remove) |
| } |
| |
| /// Remove ignored files from the working dir. |
| /// |
| /// Defaults to false. |
| pub fn remove_ignored(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_REMOVE_IGNORED, remove) |
| } |
| |
| /// Only update the contents of files that already exist. |
| /// |
| /// If set, files will not be created or deleted. |
| /// |
| /// Defaults to false. |
| pub fn update_only(&mut self, update: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_UPDATE_ONLY, update) |
| } |
| |
| /// Prevents checkout from writing the updated files' information to the |
| /// index. |
| /// |
| /// Defaults to true. |
| pub fn update_index(&mut self, update: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_DONT_UPDATE_INDEX, !update) |
| } |
| |
| /// Indicate whether the index and git attributes should be refreshed from |
| /// disk before any operations. |
| /// |
| /// Defaults to true, |
| pub fn refresh(&mut self, refresh: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_NO_REFRESH, !refresh) |
| } |
| |
| /// Skip files with unmerged index entries. |
| /// |
| /// Defaults to false. |
| pub fn skip_unmerged(&mut self, skip: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_SKIP_UNMERGED, skip) |
| } |
| |
| /// Indicate whether the checkout should proceed on conflicts by using the |
| /// stage 2 version of the file ("ours"). |
| /// |
| /// Defaults to false. |
| pub fn use_ours(&mut self, ours: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_USE_OURS, ours) |
| } |
| |
| /// Indicate whether the checkout should proceed on conflicts by using the |
| /// stage 3 version of the file ("theirs"). |
| /// |
| /// Defaults to false. |
| pub fn use_theirs(&mut self, theirs: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_USE_THEIRS, theirs) |
| } |
| |
| /// Indicate whether ignored files should be overwritten during the checkout. |
| /// |
| /// Defaults to true. |
| pub fn overwrite_ignored(&mut self, overwrite: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_DONT_OVERWRITE_IGNORED, !overwrite) |
| } |
| |
| /// Indicate whether a normal merge file should be written for conflicts. |
| /// |
| /// Defaults to false. |
| pub fn conflict_style_merge(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_MERGE, on) |
| } |
| |
| /// Specify for which notification types to invoke the notification |
| /// callback. |
| /// |
| /// Defaults to none. |
| pub fn notify_on( |
| &mut self, |
| notification_types: CheckoutNotificationType, |
| ) -> &mut CheckoutBuilder<'cb> { |
| self.notify_flags = notification_types; |
| self |
| } |
| |
| /// Indicates whether to include common ancestor data in diff3 format files |
| /// for conflicts. |
| /// |
| /// Defaults to false. |
| pub fn conflict_style_diff3(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> { |
| self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_DIFF3, on) |
| } |
| |
| /// Indicate whether to apply filters like CRLF conversion. |
| pub fn disable_filters(&mut self, disable: bool) -> &mut CheckoutBuilder<'cb> { |
| self.disable_filters = disable; |
| self |
| } |
| |
| /// Set the mode with which new directories are created. |
| /// |
| /// Default is 0755 |
| pub fn dir_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> { |
| self.dir_perm = Some(perm); |
| self |
| } |
| |
| /// Set the mode with which new files are created. |
| /// |
| /// The default is 0644 or 0755 as dictated by the blob. |
| pub fn file_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> { |
| self.file_perm = Some(perm); |
| self |
| } |
| |
| /// Add a path to be checked out. |
| /// |
| /// If no paths are specified, then all files are checked out. Otherwise |
| /// only these specified paths are checked out. |
| pub fn path<T: IntoCString>(&mut self, path: T) -> &mut CheckoutBuilder<'cb> { |
| let path = util::cstring_to_repo_path(path).unwrap(); |
| self.path_ptrs.push(path.as_ptr()); |
| self.paths.push(path); |
| self |
| } |
| |
| /// Set the directory to check out to |
| pub fn target_dir(&mut self, dst: &Path) -> &mut CheckoutBuilder<'cb> { |
| // Normal file path OK (does not need Windows conversion). |
| self.target_dir = Some(dst.into_c_string().unwrap()); |
| self |
| } |
| |
| /// The name of the common ancestor side of conflicts |
| pub fn ancestor_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> { |
| self.ancestor_label = Some(CString::new(label).unwrap()); |
| self |
| } |
| |
| /// The name of the common our side of conflicts |
| pub fn our_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> { |
| self.our_label = Some(CString::new(label).unwrap()); |
| self |
| } |
| |
| /// The name of the common their side of conflicts |
| pub fn their_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> { |
| self.their_label = Some(CString::new(label).unwrap()); |
| self |
| } |
| |
| /// Set a callback to receive notifications of checkout progress. |
| pub fn progress<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> |
| where |
| F: FnMut(Option<&Path>, usize, usize) + 'cb, |
| { |
| self.progress = Some(Box::new(cb) as Box<Progress<'cb>>); |
| self |
| } |
| |
| /// Set a callback to receive checkout notifications. |
| /// |
| /// Callbacks are invoked prior to modifying any files on disk. |
| /// Returning `false` from the callback will cancel the checkout. |
| pub fn notify<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> |
| where |
| F: FnMut( |
| CheckoutNotificationType, |
| Option<&Path>, |
| Option<DiffFile<'_>>, |
| Option<DiffFile<'_>>, |
| Option<DiffFile<'_>>, |
| ) -> bool |
| + 'cb, |
| { |
| self.notify = Some(Box::new(cb) as Box<Notify<'cb>>); |
| self |
| } |
| |
| /// Configure a raw checkout options based on this configuration. |
| /// |
| /// This method is unsafe as there is no guarantee that this structure will |
| /// outlive the provided checkout options. |
| pub unsafe fn configure(&mut self, opts: &mut raw::git_checkout_options) { |
| opts.version = raw::GIT_CHECKOUT_OPTIONS_VERSION; |
| opts.disable_filters = self.disable_filters as c_int; |
| opts.dir_mode = self.dir_perm.unwrap_or(0) as c_uint; |
| opts.file_mode = self.file_perm.unwrap_or(0) as c_uint; |
| |
| if !self.path_ptrs.is_empty() { |
| opts.paths.strings = self.path_ptrs.as_ptr() as *mut _; |
| opts.paths.count = self.path_ptrs.len() as size_t; |
| } |
| |
| if let Some(ref c) = self.target_dir { |
| opts.target_directory = c.as_ptr(); |
| } |
| if let Some(ref c) = self.ancestor_label { |
| opts.ancestor_label = c.as_ptr(); |
| } |
| if let Some(ref c) = self.our_label { |
| opts.our_label = c.as_ptr(); |
| } |
| if let Some(ref c) = self.their_label { |
| opts.their_label = c.as_ptr(); |
| } |
| if self.progress.is_some() { |
| opts.progress_cb = Some(progress_cb); |
| opts.progress_payload = self as *mut _ as *mut _; |
| } |
| if self.notify.is_some() { |
| opts.notify_cb = Some(notify_cb); |
| opts.notify_payload = self as *mut _ as *mut _; |
| opts.notify_flags = self.notify_flags.bits() as c_uint; |
| } |
| opts.checkout_strategy = self.checkout_opts as c_uint; |
| } |
| } |
| |
| extern "C" fn progress_cb( |
| path: *const c_char, |
| completed: size_t, |
| total: size_t, |
| data: *mut c_void, |
| ) { |
| panic::wrap(|| unsafe { |
| let payload = &mut *(data as *mut CheckoutBuilder<'_>); |
| let callback = match payload.progress { |
| Some(ref mut c) => c, |
| None => return, |
| }; |
| let path = if path.is_null() { |
| None |
| } else { |
| Some(util::bytes2path(CStr::from_ptr(path).to_bytes())) |
| }; |
| callback(path, completed as usize, total as usize) |
| }); |
| } |
| |
| extern "C" fn notify_cb( |
| why: raw::git_checkout_notify_t, |
| path: *const c_char, |
| baseline: *const raw::git_diff_file, |
| target: *const raw::git_diff_file, |
| workdir: *const raw::git_diff_file, |
| data: *mut c_void, |
| ) -> c_int { |
| // pack callback etc |
| panic::wrap(|| unsafe { |
| let payload = &mut *(data as *mut CheckoutBuilder<'_>); |
| let callback = match payload.notify { |
| Some(ref mut c) => c, |
| None => return 0, |
| }; |
| let path = if path.is_null() { |
| None |
| } else { |
| Some(util::bytes2path(CStr::from_ptr(path).to_bytes())) |
| }; |
| |
| let baseline = if baseline.is_null() { |
| None |
| } else { |
| Some(DiffFile::from_raw(baseline)) |
| }; |
| |
| let target = if target.is_null() { |
| None |
| } else { |
| Some(DiffFile::from_raw(target)) |
| }; |
| |
| let workdir = if workdir.is_null() { |
| None |
| } else { |
| Some(DiffFile::from_raw(workdir)) |
| }; |
| |
| let why = CheckoutNotificationType::from_bits_truncate(why as u32); |
| let keep_going = callback(why, path, baseline, target, workdir); |
| if keep_going { |
| 0 |
| } else { |
| 1 |
| } |
| }) |
| .unwrap_or(2) |
| } |
| |
| unsafe impl Send for TreeUpdateBuilder {} |
| |
| impl Default for TreeUpdateBuilder { |
| fn default() -> Self { |
| Self::new() |
| } |
| } |
| |
| impl TreeUpdateBuilder { |
| /// Create a new empty series of updates. |
| pub fn new() -> Self { |
| Self { |
| updates: Vec::new(), |
| paths: Vec::new(), |
| } |
| } |
| |
| /// Add an update removing the specified `path` from a tree. |
| pub fn remove<T: IntoCString>(&mut self, path: T) -> &mut Self { |
| let path = util::cstring_to_repo_path(path).unwrap(); |
| let path_ptr = path.as_ptr(); |
| self.paths.push(path); |
| self.updates.push(raw::git_tree_update { |
| action: raw::GIT_TREE_UPDATE_REMOVE, |
| id: raw::git_oid { |
| id: [0; raw::GIT_OID_RAWSZ], |
| }, |
| filemode: raw::GIT_FILEMODE_UNREADABLE, |
| path: path_ptr, |
| }); |
| self |
| } |
| |
| /// Add an update setting the specified `path` to a specific Oid, whether it currently exists |
| /// or not. |
| /// |
| /// Note that libgit2 does not support an upsert of a previously removed path, or an upsert |
| /// that changes the type of an object (such as from tree to blob or vice versa). |
| pub fn upsert<T: IntoCString>(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self { |
| let path = util::cstring_to_repo_path(path).unwrap(); |
| let path_ptr = path.as_ptr(); |
| self.paths.push(path); |
| self.updates.push(raw::git_tree_update { |
| action: raw::GIT_TREE_UPDATE_UPSERT, |
| id: unsafe { *id.raw() }, |
| filemode: u32::from(filemode) as raw::git_filemode_t, |
| path: path_ptr, |
| }); |
| self |
| } |
| |
| /// Create a new tree from the specified baseline and this series of updates. |
| /// |
| /// The baseline tree must exist in the specified repository. |
| pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error> { |
| let mut ret = raw::git_oid { |
| id: [0; raw::GIT_OID_RAWSZ], |
| }; |
| unsafe { |
| try_call!(raw::git_tree_create_updated( |
| &mut ret, |
| repo.raw(), |
| baseline.raw(), |
| self.updates.len(), |
| self.updates.as_ptr() |
| )); |
| Ok(Binding::from_raw(&ret as *const _)) |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::{CheckoutBuilder, RepoBuilder, TreeUpdateBuilder}; |
| use crate::{CheckoutNotificationType, FileMode, Repository}; |
| use std::fs; |
| use std::path::Path; |
| use tempfile::TempDir; |
| |
| #[test] |
| fn smoke() { |
| let r = RepoBuilder::new().clone("/path/to/nowhere", Path::new("foo")); |
| assert!(r.is_err()); |
| } |
| |
| #[test] |
| fn smoke2() { |
| let td = TempDir::new().unwrap(); |
| Repository::init_bare(&td.path().join("bare")).unwrap(); |
| let url = if cfg!(unix) { |
| format!("file://{}/bare", td.path().display()) |
| } else { |
| format!( |
| "file:///{}/bare", |
| td.path().display().to_string().replace("\\", "/") |
| ) |
| }; |
| |
| let dst = td.path().join("foo"); |
| RepoBuilder::new().clone(&url, &dst).unwrap(); |
| fs::remove_dir_all(&dst).unwrap(); |
| assert!(RepoBuilder::new().branch("foo").clone(&url, &dst).is_err()); |
| } |
| |
| #[test] |
| fn smoke_tree_create_updated() { |
| let (_tempdir, repo) = crate::test::repo_init(); |
| let (_, tree_id) = crate::test::commit(&repo); |
| let tree = t!(repo.find_tree(tree_id)); |
| assert!(tree.get_name("bar").is_none()); |
| let foo_id = tree.get_name("foo").unwrap().id(); |
| let tree2_id = t!(TreeUpdateBuilder::new() |
| .remove("foo") |
| .upsert("bar/baz", foo_id, FileMode::Blob) |
| .create_updated(&repo, &tree)); |
| let tree2 = t!(repo.find_tree(tree2_id)); |
| assert!(tree2.get_name("foo").is_none()); |
| let baz_id = tree2.get_path(Path::new("bar/baz")).unwrap().id(); |
| assert_eq!(foo_id, baz_id); |
| } |
| |
| /// Issue regression test #365 |
| #[test] |
| fn notify_callback() { |
| let td = TempDir::new().unwrap(); |
| let cd = TempDir::new().unwrap(); |
| |
| { |
| let mut opts = crate::RepositoryInitOptions::new(); |
| opts.initial_head("main"); |
| let repo = Repository::init_opts(&td.path(), &opts).unwrap(); |
| |
| let mut config = repo.config().unwrap(); |
| config.set_str("user.name", "name").unwrap(); |
| config.set_str("user.email", "email").unwrap(); |
| |
| let mut index = repo.index().unwrap(); |
| let p = Path::new(td.path()).join("file"); |
| println!("using path {:?}", p); |
| fs::File::create(&p).unwrap(); |
| index.add_path(&Path::new("file")).unwrap(); |
| let id = index.write_tree().unwrap(); |
| |
| let tree = repo.find_tree(id).unwrap(); |
| let sig = repo.signature().unwrap(); |
| repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[]) |
| .unwrap(); |
| } |
| |
| let repo = Repository::open_bare(&td.path().join(".git")).unwrap(); |
| let tree = repo |
| .revparse_single(&"main") |
| .unwrap() |
| .peel_to_tree() |
| .unwrap(); |
| let mut index = repo.index().unwrap(); |
| index.read_tree(&tree).unwrap(); |
| |
| let mut checkout_opts = CheckoutBuilder::new(); |
| checkout_opts.target_dir(&cd.path()); |
| checkout_opts.notify_on(CheckoutNotificationType::all()); |
| checkout_opts.notify(|_notif, _path, baseline, target, workdir| { |
| assert!(baseline.is_none()); |
| assert_eq!(target.unwrap().path(), Some(Path::new("file"))); |
| assert!(workdir.is_none()); |
| true |
| }); |
| repo.checkout_index(Some(&mut index), Some(&mut checkout_opts)) |
| .unwrap(); |
| } |
| } |