git2/
build.rs

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