blob: 71cf583c6c738a01049605718277d461cbb9911c [file] [log] [blame]
//! 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, Repository};
use crate::{CheckoutNotificationType, DiffFile, Remote};
/// A builder struct which is used to build configuration for cloning a new git
/// repository.
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 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)?;
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 = path.into_c_string().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> {
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() {
let f: raw::git_checkout_progress_cb = progress_cb;
opts.progress_cb = Some(f);
opts.progress_payload = self as *mut _ as *mut _;
}
if self.notify.is_some() {
let f: raw::git_checkout_notify_cb = notify_cb;
opts.notify_cb = Some(f);
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)
}
#[cfg(test)]
mod tests {
use super::{CheckoutBuilder, RepoBuilder};
use crate::{CheckoutNotificationType, Repository};
use std::fs;
use std::path::Path;
use tempdir::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("test").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());
}
/// Issue regression test #365
#[test]
fn notify_callback() {
let td = TempDir::new("test").unwrap();
let cd = TempDir::new("external-checkout").unwrap();
{
let repo = Repository::init(&td.path()).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(&"master")
.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();
}
}