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 impl Default for TreeUpdateBuilder {
default() -> Self684 fn default() -> Self {
685 Self::new()
686 }
687 }
688
689 impl TreeUpdateBuilder {
690 /// Create a new empty series of updates.
new() -> Self691 pub fn new() -> Self {
692 Self {
693 updates: Vec::new(),
694 paths: Vec::new(),
695 }
696 }
697
698 /// Add an update removing the specified `path` from a tree.
remove<T: IntoCString>(&mut self, path: T) -> &mut Self699 pub fn remove<T: IntoCString>(&mut self, path: T) -> &mut Self {
700 let path = util::cstring_to_repo_path(path).unwrap();
701 let path_ptr = path.as_ptr();
702 self.paths.push(path);
703 self.updates.push(raw::git_tree_update {
704 action: raw::GIT_TREE_UPDATE_REMOVE,
705 id: raw::git_oid {
706 id: [0; raw::GIT_OID_RAWSZ],
707 },
708 filemode: raw::GIT_FILEMODE_UNREADABLE,
709 path: path_ptr,
710 });
711 self
712 }
713
714 /// Add an update setting the specified `path` to a specific Oid, whether it currently exists
715 /// or not.
716 ///
717 /// Note that libgit2 does not support an upsert of a previously removed path, or an upsert
718 /// 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 Self719 pub fn upsert<T: IntoCString>(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self {
720 let path = util::cstring_to_repo_path(path).unwrap();
721 let path_ptr = path.as_ptr();
722 self.paths.push(path);
723 self.updates.push(raw::git_tree_update {
724 action: raw::GIT_TREE_UPDATE_UPSERT,
725 id: unsafe { *id.raw() },
726 filemode: u32::from(filemode) as raw::git_filemode_t,
727 path: path_ptr,
728 });
729 self
730 }
731
732 /// Create a new tree from the specified baseline and this series of updates.
733 ///
734 /// The baseline tree must exist in the specified repository.
create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error>735 pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error> {
736 let mut ret = raw::git_oid {
737 id: [0; raw::GIT_OID_RAWSZ],
738 };
739 unsafe {
740 try_call!(raw::git_tree_create_updated(
741 &mut ret,
742 repo.raw(),
743 baseline.raw(),
744 self.updates.len(),
745 self.updates.as_ptr()
746 ));
747 Ok(Binding::from_raw(&ret as *const _))
748 }
749 }
750 }
751
752 #[cfg(test)]
753 mod tests {
754 use super::{CheckoutBuilder, RepoBuilder, TreeUpdateBuilder};
755 use crate::{CheckoutNotificationType, FileMode, Repository};
756 use std::fs;
757 use std::path::Path;
758 use tempfile::TempDir;
759
760 #[test]
smoke()761 fn smoke() {
762 let r = RepoBuilder::new().clone("/path/to/nowhere", Path::new("foo"));
763 assert!(r.is_err());
764 }
765
766 #[test]
smoke2()767 fn smoke2() {
768 let td = TempDir::new().unwrap();
769 Repository::init_bare(&td.path().join("bare")).unwrap();
770 let url = if cfg!(unix) {
771 format!("file://{}/bare", td.path().display())
772 } else {
773 format!(
774 "file:///{}/bare",
775 td.path().display().to_string().replace("\\", "/")
776 )
777 };
778
779 let dst = td.path().join("foo");
780 RepoBuilder::new().clone(&url, &dst).unwrap();
781 fs::remove_dir_all(&dst).unwrap();
782 assert!(RepoBuilder::new().branch("foo").clone(&url, &dst).is_err());
783 }
784
785 #[test]
786 fn smoke_tree_create_updated() {
787 let (_tempdir, repo) = crate::test::repo_init();
788 let (_, tree_id) = crate::test::commit(&repo);
789 let tree = t!(repo.find_tree(tree_id));
790 assert!(tree.get_name("bar").is_none());
791 let foo_id = tree.get_name("foo").unwrap().id();
792 let tree2_id = t!(TreeUpdateBuilder::new()
793 .remove("foo")
794 .upsert("bar/baz", foo_id, FileMode::Blob)
795 .create_updated(&repo, &tree));
796 let tree2 = t!(repo.find_tree(tree2_id));
797 assert!(tree2.get_name("foo").is_none());
798 let baz_id = tree2.get_path(Path::new("bar/baz")).unwrap().id();
799 assert_eq!(foo_id, baz_id);
800 }
801
802 /// Issue regression test #365
803 #[test]
804 fn notify_callback() {
805 let td = TempDir::new().unwrap();
806 let cd = TempDir::new().unwrap();
807
808 {
809 let mut opts = crate::RepositoryInitOptions::new();
810 opts.initial_head("main");
811 let repo = Repository::init_opts(&td.path(), &opts).unwrap();
812
813 let mut config = repo.config().unwrap();
814 config.set_str("user.name", "name").unwrap();
815 config.set_str("user.email", "email").unwrap();
816
817 let mut index = repo.index().unwrap();
818 let p = Path::new(td.path()).join("file");
819 println!("using path {:?}", p);
820 fs::File::create(&p).unwrap();
821 index.add_path(&Path::new("file")).unwrap();
822 let id = index.write_tree().unwrap();
823
824 let tree = repo.find_tree(id).unwrap();
825 let sig = repo.signature().unwrap();
826 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
827 .unwrap();
828 }
829
830 let repo = Repository::open_bare(&td.path().join(".git")).unwrap();
831 let tree = repo
832 .revparse_single(&"main")
833 .unwrap()
834 .peel_to_tree()
835 .unwrap();
836 let mut index = repo.index().unwrap();
837 index.read_tree(&tree).unwrap();
838
839 let mut checkout_opts = CheckoutBuilder::new();
840 checkout_opts.target_dir(&cd.path());
841 checkout_opts.notify_on(CheckoutNotificationType::all());
842 checkout_opts.notify(|_notif, _path, baseline, target, workdir| {
843 assert!(baseline.is_none());
844 assert_eq!(target.unwrap().path(), Some(Path::new("file")));
845 assert!(workdir.is_none());
846 true
847 });
848 repo.checkout_index(Some(&mut index), Some(&mut checkout_opts))
849 .unwrap();
850 }
851 }
852