1 //! Builder-pattern objects for configuration various git operations.
2 
3 use libc::{c_char, c_int, c_uint, c_void, size_t};
4 use std::ffi::{CStr, CString};
5 use std::mem;
6 use std::path::Path;
7 use std::ptr;
8 
9 use crate::util::{self, Binding};
10 use crate::{panic, raw, Error, FetchOptions, IntoCString, Oid, Repository, Tree};
11 use crate::{CheckoutNotificationType, DiffFile, FileMode, Remote};
12 
13 /// A builder struct which is used to build configuration for cloning a new git
14 /// repository.
15 ///
16 /// # Example
17 ///
18 /// Cloning using SSH:
19 ///
20 /// ```no_run
21 /// use git2::{Cred, Error, RemoteCallbacks};
22 /// use std::env;
23 /// use std::path::Path;
24 ///
25 ///   // Prepare callbacks.
26 ///   let mut callbacks = RemoteCallbacks::new();
27 ///   callbacks.credentials(|_url, username_from_url, _allowed_types| {
28 ///     Cred::ssh_key(
29 ///       username_from_url.unwrap(),
30 ///       None,
31 ///       std::path::Path::new(&format!("{}/.ssh/id_rsa", env::var("HOME").unwrap())),
32 ///       None,
33 ///     )
34 ///   });
35 ///
36 ///   // Prepare fetch options.
37 ///   let mut fo = git2::FetchOptions::new();
38 ///   fo.remote_callbacks(callbacks);
39 ///
40 ///   // Prepare builder.
41 ///   let mut builder = git2::build::RepoBuilder::new();
42 ///   builder.fetch_options(fo);
43 ///
44 ///   // Clone the project.
45 ///   builder.clone(
46 ///     "git@github.com:rust-lang/git2-rs.git",
47 ///     Path::new("/tmp/git2-rs"),
48 ///   );
49 /// ```
50 pub struct RepoBuilder<'cb> {
51     bare: bool,
52     branch: Option<CString>,
53     local: bool,
54     hardlinks: bool,
55     checkout: Option<CheckoutBuilder<'cb>>,
56     fetch_opts: Option<FetchOptions<'cb>>,
57     clone_local: Option<CloneLocal>,
58     remote_create: Option<Box<RemoteCreate<'cb>>>,
59 }
60 
61 /// Type of callback passed to `RepoBuilder::remote_create`.
62 ///
63 /// The second and third arguments are the remote's name and the remote's url.
64 pub type RemoteCreate<'cb> =
65     dyn for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb;
66 
67 /// A builder struct for git tree updates, for use with `git_tree_create_updated`.
68 pub struct TreeUpdateBuilder {
69     updates: Vec<raw::git_tree_update>,
70     paths: Vec<CString>,
71 }
72 
73 /// A builder struct for configuring checkouts of a repository.
74 pub struct CheckoutBuilder<'cb> {
75     their_label: Option<CString>,
76     our_label: Option<CString>,
77     ancestor_label: Option<CString>,
78     target_dir: Option<CString>,
79     paths: Vec<CString>,
80     path_ptrs: Vec<*const c_char>,
81     file_perm: Option<i32>,
82     dir_perm: Option<i32>,
83     disable_filters: bool,
84     checkout_opts: u32,
85     progress: Option<Box<Progress<'cb>>>,
86     notify: Option<Box<Notify<'cb>>>,
87     notify_flags: CheckoutNotificationType,
88 }
89 
90 /// Checkout progress notification callback.
91 ///
92 /// The first argument is the path for the notification, the next is the numver
93 /// of completed steps so far, and the final is the total number of steps.
94 pub type Progress<'a> = dyn FnMut(Option<&Path>, usize, usize) + 'a;
95 
96 /// Checkout notifications callback.
97 ///
98 /// The first argument is the notification type, the next is the path for the
99 /// the notification, followed by the baseline diff, target diff, and workdir diff.
100 ///
101 /// The callback must return a bool specifying whether the checkout should
102 /// continue.
103 pub type Notify<'a> = dyn FnMut(
104         CheckoutNotificationType,
105         Option<&Path>,
106         Option<DiffFile<'_>>,
107         Option<DiffFile<'_>>,
108         Option<DiffFile<'_>>,
109     ) -> bool
110     + 'a;
111 
112 impl<'cb> Default for RepoBuilder<'cb> {
default() -> Self113     fn default() -> Self {
114         Self::new()
115     }
116 }
117 
118 /// Options that can be passed to `RepoBuilder::clone_local`.
119 #[derive(Clone, Copy)]
120 pub enum CloneLocal {
121     /// Auto-detect (default)
122     ///
123     /// Here libgit2 will bypass the git-aware transport for local paths, but
124     /// use a normal fetch for `file://` urls.
125     Auto = raw::GIT_CLONE_LOCAL_AUTO as isize,
126 
127     /// Bypass the git-aware transport even for `file://` urls.
128     Local = raw::GIT_CLONE_LOCAL as isize,
129 
130     /// Never bypass the git-aware transport
131     None = raw::GIT_CLONE_NO_LOCAL as isize,
132 
133     /// Bypass the git-aware transport, but don't try to use hardlinks.
134     NoLinks = raw::GIT_CLONE_LOCAL_NO_LINKS as isize,
135 
136     #[doc(hidden)]
137     __Nonexhaustive = 0xff,
138 }
139 
140 impl<'cb> RepoBuilder<'cb> {
141     /// Creates a new repository builder with all of the default configuration.
142     ///
143     /// When ready, the `clone()` method can be used to clone a new repository
144     /// using this configuration.
new() -> RepoBuilder<'cb>145     pub fn new() -> RepoBuilder<'cb> {
146         crate::init();
147         RepoBuilder {
148             bare: false,
149             branch: None,
150             local: true,
151             clone_local: None,
152             hardlinks: true,
153             checkout: None,
154             fetch_opts: None,
155             remote_create: None,
156         }
157     }
158 
159     /// Indicate whether the repository will be cloned as a bare repository or
160     /// not.
bare(&mut self, bare: bool) -> &mut RepoBuilder<'cb>161     pub fn bare(&mut self, bare: bool) -> &mut RepoBuilder<'cb> {
162         self.bare = bare;
163         self
164     }
165 
166     /// Specify the name of the branch to check out after the clone.
167     ///
168     /// If not specified, the remote's default branch will be used.
branch(&mut self, branch: &str) -> &mut RepoBuilder<'cb>169     pub fn branch(&mut self, branch: &str) -> &mut RepoBuilder<'cb> {
170         self.branch = Some(CString::new(branch).unwrap());
171         self
172     }
173 
174     /// Configures options for bypassing the git-aware transport on clone.
175     ///
176     /// Bypassing it means that instead of a fetch libgit2 will copy the object
177     /// database directory instead of figuring out what it needs, which is
178     /// faster. If possible, it will hardlink the files to save space.
clone_local(&mut self, clone_local: CloneLocal) -> &mut RepoBuilder<'cb>179     pub fn clone_local(&mut self, clone_local: CloneLocal) -> &mut RepoBuilder<'cb> {
180         self.clone_local = Some(clone_local);
181         self
182     }
183 
184     /// Set the flag for bypassing the git aware transport mechanism for local
185     /// paths.
186     ///
187     /// If `true`, the git-aware transport will be bypassed for local paths. If
188     /// `false`, the git-aware transport will not be bypassed.
189     #[deprecated(note = "use `clone_local` instead")]
190     #[doc(hidden)]
local(&mut self, local: bool) -> &mut RepoBuilder<'cb>191     pub fn local(&mut self, local: bool) -> &mut RepoBuilder<'cb> {
192         self.local = local;
193         self
194     }
195 
196     /// Set the flag for whether hardlinks are used when using a local git-aware
197     /// transport mechanism.
198     #[deprecated(note = "use `clone_local` instead")]
199     #[doc(hidden)]
hardlinks(&mut self, links: bool) -> &mut RepoBuilder<'cb>200     pub fn hardlinks(&mut self, links: bool) -> &mut RepoBuilder<'cb> {
201         self.hardlinks = links;
202         self
203     }
204 
205     /// Configure the checkout which will be performed by consuming a checkout
206     /// builder.
with_checkout(&mut self, checkout: CheckoutBuilder<'cb>) -> &mut RepoBuilder<'cb>207     pub fn with_checkout(&mut self, checkout: CheckoutBuilder<'cb>) -> &mut RepoBuilder<'cb> {
208         self.checkout = Some(checkout);
209         self
210     }
211 
212     /// Options which control the fetch, including callbacks.
213     ///
214     /// The callbacks are used for reporting fetch progress, and for acquiring
215     /// credentials in the event they are needed.
fetch_options(&mut self, fetch_opts: FetchOptions<'cb>) -> &mut RepoBuilder<'cb>216     pub fn fetch_options(&mut self, fetch_opts: FetchOptions<'cb>) -> &mut RepoBuilder<'cb> {
217         self.fetch_opts = Some(fetch_opts);
218         self
219     }
220 
221     /// Configures a callback used to create the git remote, prior to its being
222     /// used to perform the clone operation.
remote_create<F>(&mut self, f: F) -> &mut RepoBuilder<'cb> where F: for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb,223     pub fn remote_create<F>(&mut self, f: F) -> &mut RepoBuilder<'cb>
224     where
225         F: for<'a> FnMut(&'a Repository, &str, &str) -> Result<Remote<'a>, Error> + 'cb,
226     {
227         self.remote_create = Some(Box::new(f));
228         self
229     }
230 
231     /// Clone a remote repository.
232     ///
233     /// This will use the options configured so far to clone the specified url
234     /// into the specified local path.
clone(&mut self, url: &str, into: &Path) -> Result<Repository, Error>235     pub fn clone(&mut self, url: &str, into: &Path) -> Result<Repository, Error> {
236         let mut opts: raw::git_clone_options = unsafe { mem::zeroed() };
237         unsafe {
238             try_call!(raw::git_clone_init_options(
239                 &mut opts,
240                 raw::GIT_CLONE_OPTIONS_VERSION
241             ));
242         }
243         opts.bare = self.bare as c_int;
244         opts.checkout_branch = self
245             .branch
246             .as_ref()
247             .map(|s| s.as_ptr())
248             .unwrap_or(ptr::null());
249 
250         if let Some(ref local) = self.clone_local {
251             opts.local = *local as raw::git_clone_local_t;
252         } else {
253             opts.local = match (self.local, self.hardlinks) {
254                 (true, false) => raw::GIT_CLONE_LOCAL_NO_LINKS,
255                 (false, _) => raw::GIT_CLONE_NO_LOCAL,
256                 (true, _) => raw::GIT_CLONE_LOCAL_AUTO,
257             };
258         }
259 
260         if let Some(ref mut cbs) = self.fetch_opts {
261             opts.fetch_opts = cbs.raw();
262         }
263 
264         if let Some(ref mut c) = self.checkout {
265             unsafe {
266                 c.configure(&mut opts.checkout_opts);
267             }
268         }
269 
270         if let Some(ref mut callback) = self.remote_create {
271             opts.remote_cb = Some(remote_create_cb);
272             opts.remote_cb_payload = callback as *mut _ as *mut _;
273         }
274 
275         let url = CString::new(url)?;
276         // Normal file path OK (does not need Windows conversion).
277         let into = into.into_c_string()?;
278         let mut raw = ptr::null_mut();
279         unsafe {
280             try_call!(raw::git_clone(&mut raw, url, into, &opts));
281             Ok(Binding::from_raw(raw))
282         }
283     }
284 }
285 
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_int286 extern "C" fn remote_create_cb(
287     out: *mut *mut raw::git_remote,
288     repo: *mut raw::git_repository,
289     name: *const c_char,
290     url: *const c_char,
291     payload: *mut c_void,
292 ) -> c_int {
293     unsafe {
294         let repo = Repository::from_raw(repo);
295         let code = panic::wrap(|| {
296             let name = CStr::from_ptr(name).to_str().unwrap();
297             let url = CStr::from_ptr(url).to_str().unwrap();
298             let f = payload as *mut Box<RemoteCreate<'_>>;
299             match (*f)(&repo, name, url) {
300                 Ok(remote) => {
301                     *out = crate::remote::remote_into_raw(remote);
302                     0
303                 }
304                 Err(e) => e.raw_code(),
305             }
306         });
307         mem::forget(repo);
308         code.unwrap_or(-1)
309     }
310 }
311 
312 impl<'cb> Default for CheckoutBuilder<'cb> {
default() -> Self313     fn default() -> Self {
314         Self::new()
315     }
316 }
317 
318 impl<'cb> CheckoutBuilder<'cb> {
319     /// Creates a new builder for checkouts with all of its default
320     /// configuration.
new() -> CheckoutBuilder<'cb>321     pub fn new() -> CheckoutBuilder<'cb> {
322         crate::init();
323         CheckoutBuilder {
324             disable_filters: false,
325             dir_perm: None,
326             file_perm: None,
327             path_ptrs: Vec::new(),
328             paths: Vec::new(),
329             target_dir: None,
330             ancestor_label: None,
331             our_label: None,
332             their_label: None,
333             checkout_opts: raw::GIT_CHECKOUT_SAFE as u32,
334             progress: None,
335             notify: None,
336             notify_flags: CheckoutNotificationType::empty(),
337         }
338     }
339 
340     /// Indicate that this checkout should perform a dry run by checking for
341     /// conflicts but not make any actual changes.
dry_run(&mut self) -> &mut CheckoutBuilder<'cb>342     pub fn dry_run(&mut self) -> &mut CheckoutBuilder<'cb> {
343         self.checkout_opts &= !((1 << 4) - 1);
344         self.checkout_opts |= raw::GIT_CHECKOUT_NONE as u32;
345         self
346     }
347 
348     /// Take any action necessary to get the working directory to match the
349     /// target including potentially discarding modified files.
force(&mut self) -> &mut CheckoutBuilder<'cb>350     pub fn force(&mut self) -> &mut CheckoutBuilder<'cb> {
351         self.checkout_opts &= !((1 << 4) - 1);
352         self.checkout_opts |= raw::GIT_CHECKOUT_FORCE as u32;
353         self
354     }
355 
356     /// Indicate that the checkout should be performed safely, allowing new
357     /// files to be created but not overwriting extisting files or changes.
358     ///
359     /// This is the default.
safe(&mut self) -> &mut CheckoutBuilder<'cb>360     pub fn safe(&mut self) -> &mut CheckoutBuilder<'cb> {
361         self.checkout_opts &= !((1 << 4) - 1);
362         self.checkout_opts |= raw::GIT_CHECKOUT_SAFE as u32;
363         self
364     }
365 
flag(&mut self, bit: raw::git_checkout_strategy_t, on: bool) -> &mut CheckoutBuilder<'cb>366     fn flag(&mut self, bit: raw::git_checkout_strategy_t, on: bool) -> &mut CheckoutBuilder<'cb> {
367         if on {
368             self.checkout_opts |= bit as u32;
369         } else {
370             self.checkout_opts &= !(bit as u32);
371         }
372         self
373     }
374 
375     /// In safe mode, create files that don't exist.
376     ///
377     /// Defaults to false.
recreate_missing(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb>378     pub fn recreate_missing(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb> {
379         self.flag(raw::GIT_CHECKOUT_RECREATE_MISSING, allow)
380     }
381 
382     /// In safe mode, apply safe file updates even when there are conflicts
383     /// instead of canceling the checkout.
384     ///
385     /// Defaults to false.
allow_conflicts(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb>386     pub fn allow_conflicts(&mut self, allow: bool) -> &mut CheckoutBuilder<'cb> {
387         self.flag(raw::GIT_CHECKOUT_ALLOW_CONFLICTS, allow)
388     }
389 
390     /// Remove untracked files from the working dir.
391     ///
392     /// Defaults to false.
remove_untracked(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb>393     pub fn remove_untracked(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> {
394         self.flag(raw::GIT_CHECKOUT_REMOVE_UNTRACKED, remove)
395     }
396 
397     /// Remove ignored files from the working dir.
398     ///
399     /// Defaults to false.
remove_ignored(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb>400     pub fn remove_ignored(&mut self, remove: bool) -> &mut CheckoutBuilder<'cb> {
401         self.flag(raw::GIT_CHECKOUT_REMOVE_IGNORED, remove)
402     }
403 
404     /// Only update the contents of files that already exist.
405     ///
406     /// If set, files will not be created or deleted.
407     ///
408     /// Defaults to false.
update_only(&mut self, update: bool) -> &mut CheckoutBuilder<'cb>409     pub fn update_only(&mut self, update: bool) -> &mut CheckoutBuilder<'cb> {
410         self.flag(raw::GIT_CHECKOUT_UPDATE_ONLY, update)
411     }
412 
413     /// Prevents checkout from writing the updated files' information to the
414     /// index.
415     ///
416     /// Defaults to true.
update_index(&mut self, update: bool) -> &mut CheckoutBuilder<'cb>417     pub fn update_index(&mut self, update: bool) -> &mut CheckoutBuilder<'cb> {
418         self.flag(raw::GIT_CHECKOUT_DONT_UPDATE_INDEX, !update)
419     }
420 
421     /// Indicate whether the index and git attributes should be refreshed from
422     /// disk before any operations.
423     ///
424     /// Defaults to true,
refresh(&mut self, refresh: bool) -> &mut CheckoutBuilder<'cb>425     pub fn refresh(&mut self, refresh: bool) -> &mut CheckoutBuilder<'cb> {
426         self.flag(raw::GIT_CHECKOUT_NO_REFRESH, !refresh)
427     }
428 
429     /// Skip files with unmerged index entries.
430     ///
431     /// Defaults to false.
skip_unmerged(&mut self, skip: bool) -> &mut CheckoutBuilder<'cb>432     pub fn skip_unmerged(&mut self, skip: bool) -> &mut CheckoutBuilder<'cb> {
433         self.flag(raw::GIT_CHECKOUT_SKIP_UNMERGED, skip)
434     }
435 
436     /// Indicate whether the checkout should proceed on conflicts by using the
437     /// stage 2 version of the file ("ours").
438     ///
439     /// Defaults to false.
use_ours(&mut self, ours: bool) -> &mut CheckoutBuilder<'cb>440     pub fn use_ours(&mut self, ours: bool) -> &mut CheckoutBuilder<'cb> {
441         self.flag(raw::GIT_CHECKOUT_USE_OURS, ours)
442     }
443 
444     /// Indicate whether the checkout should proceed on conflicts by using the
445     /// stage 3 version of the file ("theirs").
446     ///
447     /// Defaults to false.
use_theirs(&mut self, theirs: bool) -> &mut CheckoutBuilder<'cb>448     pub fn use_theirs(&mut self, theirs: bool) -> &mut CheckoutBuilder<'cb> {
449         self.flag(raw::GIT_CHECKOUT_USE_THEIRS, theirs)
450     }
451 
452     /// Indicate whether ignored files should be overwritten during the checkout.
453     ///
454     /// Defaults to true.
overwrite_ignored(&mut self, overwrite: bool) -> &mut CheckoutBuilder<'cb>455     pub fn overwrite_ignored(&mut self, overwrite: bool) -> &mut CheckoutBuilder<'cb> {
456         self.flag(raw::GIT_CHECKOUT_DONT_OVERWRITE_IGNORED, !overwrite)
457     }
458 
459     /// Indicate whether a normal merge file should be written for conflicts.
460     ///
461     /// Defaults to false.
conflict_style_merge(&mut self, on: bool) -> &mut CheckoutBuilder<'cb>462     pub fn conflict_style_merge(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> {
463         self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_MERGE, on)
464     }
465 
466     /// Specify for which notification types to invoke the notification
467     /// callback.
468     ///
469     /// Defaults to none.
notify_on( &mut self, notification_types: CheckoutNotificationType, ) -> &mut CheckoutBuilder<'cb>470     pub fn notify_on(
471         &mut self,
472         notification_types: CheckoutNotificationType,
473     ) -> &mut CheckoutBuilder<'cb> {
474         self.notify_flags = notification_types;
475         self
476     }
477 
478     /// Indicates whether to include common ancestor data in diff3 format files
479     /// for conflicts.
480     ///
481     /// Defaults to false.
conflict_style_diff3(&mut self, on: bool) -> &mut CheckoutBuilder<'cb>482     pub fn conflict_style_diff3(&mut self, on: bool) -> &mut CheckoutBuilder<'cb> {
483         self.flag(raw::GIT_CHECKOUT_CONFLICT_STYLE_DIFF3, on)
484     }
485 
486     /// Indicate whether to apply filters like CRLF conversion.
disable_filters(&mut self, disable: bool) -> &mut CheckoutBuilder<'cb>487     pub fn disable_filters(&mut self, disable: bool) -> &mut CheckoutBuilder<'cb> {
488         self.disable_filters = disable;
489         self
490     }
491 
492     /// Set the mode with which new directories are created.
493     ///
494     /// Default is 0755
dir_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb>495     pub fn dir_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> {
496         self.dir_perm = Some(perm);
497         self
498     }
499 
500     /// Set the mode with which new files are created.
501     ///
502     /// The default is 0644 or 0755 as dictated by the blob.
file_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb>503     pub fn file_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> {
504         self.file_perm = Some(perm);
505         self
506     }
507 
508     /// Add a path to be checked out.
509     ///
510     /// If no paths are specified, then all files are checked out. Otherwise
511     /// only these specified paths are checked out.
path<T: IntoCString>(&mut self, path: T) -> &mut CheckoutBuilder<'cb>512     pub fn path<T: IntoCString>(&mut self, path: T) -> &mut CheckoutBuilder<'cb> {
513         let path = util::cstring_to_repo_path(path).unwrap();
514         self.path_ptrs.push(path.as_ptr());
515         self.paths.push(path);
516         self
517     }
518 
519     /// Set the directory to check out to
target_dir(&mut self, dst: &Path) -> &mut CheckoutBuilder<'cb>520     pub fn target_dir(&mut self, dst: &Path) -> &mut CheckoutBuilder<'cb> {
521         // Normal file path OK (does not need Windows conversion).
522         self.target_dir = Some(dst.into_c_string().unwrap());
523         self
524     }
525 
526     /// The name of the common ancestor side of conflicts
ancestor_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb>527     pub fn ancestor_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> {
528         self.ancestor_label = Some(CString::new(label).unwrap());
529         self
530     }
531 
532     /// The name of the common our side of conflicts
our_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb>533     pub fn our_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> {
534         self.our_label = Some(CString::new(label).unwrap());
535         self
536     }
537 
538     /// The name of the common their side of conflicts
their_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb>539     pub fn their_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> {
540         self.their_label = Some(CString::new(label).unwrap());
541         self
542     }
543 
544     /// Set a callback to receive notifications of checkout progress.
progress<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> where F: FnMut(Option<&Path>, usize, usize) + 'cb,545     pub fn progress<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb>
546     where
547         F: FnMut(Option<&Path>, usize, usize) + 'cb,
548     {
549         self.progress = Some(Box::new(cb) as Box<Progress<'cb>>);
550         self
551     }
552 
553     /// Set a callback to receive checkout notifications.
554     ///
555     /// Callbacks are invoked prior to modifying any files on disk.
556     /// Returning `false` from the callback will cancel the checkout.
notify<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb> where F: FnMut( CheckoutNotificationType, Option<&Path>, Option<DiffFile<'_>>, Option<DiffFile<'_>>, Option<DiffFile<'_>>, ) -> bool + 'cb,557     pub fn notify<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb>
558     where
559         F: FnMut(
560                 CheckoutNotificationType,
561                 Option<&Path>,
562                 Option<DiffFile<'_>>,
563                 Option<DiffFile<'_>>,
564                 Option<DiffFile<'_>>,
565             ) -> bool
566             + 'cb,
567     {
568         self.notify = Some(Box::new(cb) as Box<Notify<'cb>>);
569         self
570     }
571 
572     /// Configure a raw checkout options based on this configuration.
573     ///
574     /// This method is unsafe as there is no guarantee that this structure will
575     /// outlive the provided checkout options.
configure(&mut self, opts: &mut raw::git_checkout_options)576     pub unsafe fn configure(&mut self, opts: &mut raw::git_checkout_options) {
577         opts.version = raw::GIT_CHECKOUT_OPTIONS_VERSION;
578         opts.disable_filters = self.disable_filters as c_int;
579         opts.dir_mode = self.dir_perm.unwrap_or(0) as c_uint;
580         opts.file_mode = self.file_perm.unwrap_or(0) as c_uint;
581 
582         if !self.path_ptrs.is_empty() {
583             opts.paths.strings = self.path_ptrs.as_ptr() as *mut _;
584             opts.paths.count = self.path_ptrs.len() as size_t;
585         }
586 
587         if let Some(ref c) = self.target_dir {
588             opts.target_directory = c.as_ptr();
589         }
590         if let Some(ref c) = self.ancestor_label {
591             opts.ancestor_label = c.as_ptr();
592         }
593         if let Some(ref c) = self.our_label {
594             opts.our_label = c.as_ptr();
595         }
596         if let Some(ref c) = self.their_label {
597             opts.their_label = c.as_ptr();
598         }
599         if self.progress.is_some() {
600             opts.progress_cb = Some(progress_cb);
601             opts.progress_payload = self as *mut _ as *mut _;
602         }
603         if self.notify.is_some() {
604             opts.notify_cb = Some(notify_cb);
605             opts.notify_payload = self as *mut _ as *mut _;
606             opts.notify_flags = self.notify_flags.bits() as c_uint;
607         }
608         opts.checkout_strategy = self.checkout_opts as c_uint;
609     }
610 }
611 
progress_cb( path: *const c_char, completed: size_t, total: size_t, data: *mut c_void, )612 extern "C" fn progress_cb(
613     path: *const c_char,
614     completed: size_t,
615     total: size_t,
616     data: *mut c_void,
617 ) {
618     panic::wrap(|| unsafe {
619         let payload = &mut *(data as *mut CheckoutBuilder<'_>);
620         let callback = match payload.progress {
621             Some(ref mut c) => c,
622             None => return,
623         };
624         let path = if path.is_null() {
625             None
626         } else {
627             Some(util::bytes2path(CStr::from_ptr(path).to_bytes()))
628         };
629         callback(path, completed as usize, total as usize)
630     });
631 }
632 
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_int633 extern "C" fn notify_cb(
634     why: raw::git_checkout_notify_t,
635     path: *const c_char,
636     baseline: *const raw::git_diff_file,
637     target: *const raw::git_diff_file,
638     workdir: *const raw::git_diff_file,
639     data: *mut c_void,
640 ) -> c_int {
641     // pack callback etc
642     panic::wrap(|| unsafe {
643         let payload = &mut *(data as *mut CheckoutBuilder<'_>);
644         let callback = match payload.notify {
645             Some(ref mut c) => c,
646             None => return 0,
647         };
648         let path = if path.is_null() {
649             None
650         } else {
651             Some(util::bytes2path(CStr::from_ptr(path).to_bytes()))
652         };
653 
654         let baseline = if baseline.is_null() {
655             None
656         } else {
657             Some(DiffFile::from_raw(baseline))
658         };
659 
660         let target = if target.is_null() {
661             None
662         } else {
663             Some(DiffFile::from_raw(target))
664         };
665 
666         let workdir = if workdir.is_null() {
667             None
668         } else {
669             Some(DiffFile::from_raw(workdir))
670         };
671 
672         let why = CheckoutNotificationType::from_bits_truncate(why as u32);
673         let keep_going = callback(why, path, baseline, target, workdir);
674         if keep_going {
675             0
676         } else {
677             1
678         }
679     })
680     .unwrap_or(2)
681 }
682 
683 unsafe impl Send for TreeUpdateBuilder {}
684 
685 impl Default for TreeUpdateBuilder {
default() -> Self686     fn default() -> Self {
687         Self::new()
688     }
689 }
690 
691 impl TreeUpdateBuilder {
692     /// Create a new empty series of updates.
new() -> Self693     pub fn new() -> Self {
694         Self {
695             updates: Vec::new(),
696             paths: Vec::new(),
697         }
698     }
699 
700     /// Add an update removing the specified `path` from a tree.
remove<T: IntoCString>(&mut self, path: T) -> &mut Self701     pub fn remove<T: IntoCString>(&mut self, path: T) -> &mut Self {
702         let path = util::cstring_to_repo_path(path).unwrap();
703         let path_ptr = path.as_ptr();
704         self.paths.push(path);
705         self.updates.push(raw::git_tree_update {
706             action: raw::GIT_TREE_UPDATE_REMOVE,
707             id: raw::git_oid {
708                 id: [0; raw::GIT_OID_RAWSZ],
709             },
710             filemode: raw::GIT_FILEMODE_UNREADABLE,
711             path: path_ptr,
712         });
713         self
714     }
715 
716     /// Add an update setting the specified `path` to a specific Oid, whether it currently exists
717     /// or not.
718     ///
719     /// Note that libgit2 does not support an upsert of a previously removed path, or an upsert
720     /// that changes the type of an object (such as from tree to blob or vice versa).
upsert<T: IntoCString>(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self721     pub fn upsert<T: IntoCString>(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self {
722         let path = util::cstring_to_repo_path(path).unwrap();
723         let path_ptr = path.as_ptr();
724         self.paths.push(path);
725         self.updates.push(raw::git_tree_update {
726             action: raw::GIT_TREE_UPDATE_UPSERT,
727             id: unsafe { *id.raw() },
728             filemode: u32::from(filemode) as raw::git_filemode_t,
729             path: path_ptr,
730         });
731         self
732     }
733 
734     /// Create a new tree from the specified baseline and this series of updates.
735     ///
736     /// The baseline tree must exist in the specified repository.
create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error>737     pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error> {
738         let mut ret = raw::git_oid {
739             id: [0; raw::GIT_OID_RAWSZ],
740         };
741         unsafe {
742             try_call!(raw::git_tree_create_updated(
743                 &mut ret,
744                 repo.raw(),
745                 baseline.raw(),
746                 self.updates.len(),
747                 self.updates.as_ptr()
748             ));
749             Ok(Binding::from_raw(&ret as *const _))
750         }
751     }
752 }
753 
754 #[cfg(test)]
755 mod tests {
756     use super::{CheckoutBuilder, RepoBuilder, TreeUpdateBuilder};
757     use crate::{CheckoutNotificationType, FileMode, Repository};
758     use std::fs;
759     use std::path::Path;
760     use tempfile::TempDir;
761 
762     #[test]
smoke()763     fn smoke() {
764         let r = RepoBuilder::new().clone("/path/to/nowhere", Path::new("foo"));
765         assert!(r.is_err());
766     }
767 
768     #[test]
smoke2()769     fn smoke2() {
770         let td = TempDir::new().unwrap();
771         Repository::init_bare(&td.path().join("bare")).unwrap();
772         let url = if cfg!(unix) {
773             format!("file://{}/bare", td.path().display())
774         } else {
775             format!(
776                 "file:///{}/bare",
777                 td.path().display().to_string().replace("\\", "/")
778             )
779         };
780 
781         let dst = td.path().join("foo");
782         RepoBuilder::new().clone(&url, &dst).unwrap();
783         fs::remove_dir_all(&dst).unwrap();
784         assert!(RepoBuilder::new().branch("foo").clone(&url, &dst).is_err());
785     }
786 
787     #[test]
788     fn smoke_tree_create_updated() {
789         let (_tempdir, repo) = crate::test::repo_init();
790         let (_, tree_id) = crate::test::commit(&repo);
791         let tree = t!(repo.find_tree(tree_id));
792         assert!(tree.get_name("bar").is_none());
793         let foo_id = tree.get_name("foo").unwrap().id();
794         let tree2_id = t!(TreeUpdateBuilder::new()
795             .remove("foo")
796             .upsert("bar/baz", foo_id, FileMode::Blob)
797             .create_updated(&repo, &tree));
798         let tree2 = t!(repo.find_tree(tree2_id));
799         assert!(tree2.get_name("foo").is_none());
800         let baz_id = tree2.get_path(Path::new("bar/baz")).unwrap().id();
801         assert_eq!(foo_id, baz_id);
802     }
803 
804     /// Issue regression test #365
805     #[test]
806     fn notify_callback() {
807         let td = TempDir::new().unwrap();
808         let cd = TempDir::new().unwrap();
809 
810         {
811             let mut opts = crate::RepositoryInitOptions::new();
812             opts.initial_head("main");
813             let repo = Repository::init_opts(&td.path(), &opts).unwrap();
814 
815             let mut config = repo.config().unwrap();
816             config.set_str("user.name", "name").unwrap();
817             config.set_str("user.email", "email").unwrap();
818 
819             let mut index = repo.index().unwrap();
820             let p = Path::new(td.path()).join("file");
821             println!("using path {:?}", p);
822             fs::File::create(&p).unwrap();
823             index.add_path(&Path::new("file")).unwrap();
824             let id = index.write_tree().unwrap();
825 
826             let tree = repo.find_tree(id).unwrap();
827             let sig = repo.signature().unwrap();
828             repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
829                 .unwrap();
830         }
831 
832         let repo = Repository::open_bare(&td.path().join(".git")).unwrap();
833         let tree = repo
834             .revparse_single(&"main")
835             .unwrap()
836             .peel_to_tree()
837             .unwrap();
838         let mut index = repo.index().unwrap();
839         index.read_tree(&tree).unwrap();
840 
841         let mut checkout_opts = CheckoutBuilder::new();
842         checkout_opts.target_dir(&cd.path());
843         checkout_opts.notify_on(CheckoutNotificationType::all());
844         checkout_opts.notify(|_notif, _path, baseline, target, workdir| {
845             assert!(baseline.is_none());
846             assert_eq!(target.unwrap().path(), Some(Path::new("file")));
847             assert!(workdir.is_none());
848             true
849         });
850         repo.checkout_index(Some(&mut index), Some(&mut checkout_opts))
851             .unwrap();
852     }
853 }
854