]> git.proxmox.com Git - rustc.git/blob - extra/git2/src/build.rs
New upstream version 1.73.0+dfsg1
[rustc.git] / extra / git2 / src / build.rs
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 /// 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.
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
76 pub 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.
82 pub 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.
102 pub 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.
111 pub type Notify<'a> = dyn FnMut(
112 CheckoutNotificationType,
113 Option<&Path>,
114 Option<DiffFile<'_>>,
115 Option<DiffFile<'_>>,
116 Option<DiffFile<'_>>,
117 ) -> bool
118 + 'a;
119
120 impl<'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)]
128 pub 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
148 impl<'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
294 extern "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
320 impl<'cb> Default for CheckoutBuilder<'cb> {
321 fn default() -> Self {
322 Self::new()
323 }
324 }
325
326 impl<'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 /// Indicate whether to apply filters like CRLF conversion.
495 pub fn disable_filters(&mut self, disable: bool) -> &mut CheckoutBuilder<'cb> {
496 self.disable_filters = disable;
497 self
498 }
499
500 /// Set the mode with which new directories are created.
501 ///
502 /// Default is 0755
503 pub fn dir_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> {
504 self.dir_perm = Some(perm);
505 self
506 }
507
508 /// Set the mode with which new files are created.
509 ///
510 /// The default is 0644 or 0755 as dictated by the blob.
511 pub fn file_perm(&mut self, perm: i32) -> &mut CheckoutBuilder<'cb> {
512 self.file_perm = Some(perm);
513 self
514 }
515
516 /// Add a path to be checked out.
517 ///
518 /// If no paths are specified, then all files are checked out. Otherwise
519 /// only these specified paths are checked out.
520 pub fn path<T: IntoCString>(&mut self, path: T) -> &mut CheckoutBuilder<'cb> {
521 let path = util::cstring_to_repo_path(path).unwrap();
522 self.path_ptrs.push(path.as_ptr());
523 self.paths.push(path);
524 self
525 }
526
527 /// Set the directory to check out to
528 pub fn target_dir(&mut self, dst: &Path) -> &mut CheckoutBuilder<'cb> {
529 // Normal file path OK (does not need Windows conversion).
530 self.target_dir = Some(dst.into_c_string().unwrap());
531 self
532 }
533
534 /// The name of the common ancestor side of conflicts
535 pub fn ancestor_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> {
536 self.ancestor_label = Some(CString::new(label).unwrap());
537 self
538 }
539
540 /// The name of the common our side of conflicts
541 pub fn our_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> {
542 self.our_label = Some(CString::new(label).unwrap());
543 self
544 }
545
546 /// The name of the common their side of conflicts
547 pub fn their_label(&mut self, label: &str) -> &mut CheckoutBuilder<'cb> {
548 self.their_label = Some(CString::new(label).unwrap());
549 self
550 }
551
552 /// Set a callback to receive notifications of checkout progress.
553 pub fn progress<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb>
554 where
555 F: FnMut(Option<&Path>, usize, usize) + 'cb,
556 {
557 self.progress = Some(Box::new(cb) as Box<Progress<'cb>>);
558 self
559 }
560
561 /// Set a callback to receive checkout notifications.
562 ///
563 /// Callbacks are invoked prior to modifying any files on disk.
564 /// Returning `false` from the callback will cancel the checkout.
565 pub fn notify<F>(&mut self, cb: F) -> &mut CheckoutBuilder<'cb>
566 where
567 F: FnMut(
568 CheckoutNotificationType,
569 Option<&Path>,
570 Option<DiffFile<'_>>,
571 Option<DiffFile<'_>>,
572 Option<DiffFile<'_>>,
573 ) -> bool
574 + 'cb,
575 {
576 self.notify = Some(Box::new(cb) as Box<Notify<'cb>>);
577 self
578 }
579
580 /// Configure a raw checkout options based on this configuration.
581 ///
582 /// This method is unsafe as there is no guarantee that this structure will
583 /// outlive the provided checkout options.
584 pub unsafe fn configure(&mut self, opts: &mut raw::git_checkout_options) {
585 opts.version = raw::GIT_CHECKOUT_OPTIONS_VERSION;
586 opts.disable_filters = self.disable_filters as c_int;
587 opts.dir_mode = self.dir_perm.unwrap_or(0) as c_uint;
588 opts.file_mode = self.file_perm.unwrap_or(0) as c_uint;
589
590 if !self.path_ptrs.is_empty() {
591 opts.paths.strings = self.path_ptrs.as_ptr() as *mut _;
592 opts.paths.count = self.path_ptrs.len() as size_t;
593 }
594
595 if let Some(ref c) = self.target_dir {
596 opts.target_directory = c.as_ptr();
597 }
598 if let Some(ref c) = self.ancestor_label {
599 opts.ancestor_label = c.as_ptr();
600 }
601 if let Some(ref c) = self.our_label {
602 opts.our_label = c.as_ptr();
603 }
604 if let Some(ref c) = self.their_label {
605 opts.their_label = c.as_ptr();
606 }
607 if self.progress.is_some() {
608 opts.progress_cb = Some(progress_cb);
609 opts.progress_payload = self as *mut _ as *mut _;
610 }
611 if self.notify.is_some() {
612 opts.notify_cb = Some(notify_cb);
613 opts.notify_payload = self as *mut _ as *mut _;
614 opts.notify_flags = self.notify_flags.bits() as c_uint;
615 }
616 opts.checkout_strategy = self.checkout_opts as c_uint;
617 }
618 }
619
620 extern "C" fn progress_cb(
621 path: *const c_char,
622 completed: size_t,
623 total: size_t,
624 data: *mut c_void,
625 ) {
626 panic::wrap(|| unsafe {
627 let payload = &mut *(data as *mut CheckoutBuilder<'_>);
628 let callback = match payload.progress {
629 Some(ref mut c) => c,
630 None => return,
631 };
632 let path = if path.is_null() {
633 None
634 } else {
635 Some(util::bytes2path(CStr::from_ptr(path).to_bytes()))
636 };
637 callback(path, completed as usize, total as usize)
638 });
639 }
640
641 extern "C" fn notify_cb(
642 why: raw::git_checkout_notify_t,
643 path: *const c_char,
644 baseline: *const raw::git_diff_file,
645 target: *const raw::git_diff_file,
646 workdir: *const raw::git_diff_file,
647 data: *mut c_void,
648 ) -> c_int {
649 // pack callback etc
650 panic::wrap(|| unsafe {
651 let payload = &mut *(data as *mut CheckoutBuilder<'_>);
652 let callback = match payload.notify {
653 Some(ref mut c) => c,
654 None => return 0,
655 };
656 let path = if path.is_null() {
657 None
658 } else {
659 Some(util::bytes2path(CStr::from_ptr(path).to_bytes()))
660 };
661
662 let baseline = if baseline.is_null() {
663 None
664 } else {
665 Some(DiffFile::from_raw(baseline))
666 };
667
668 let target = if target.is_null() {
669 None
670 } else {
671 Some(DiffFile::from_raw(target))
672 };
673
674 let workdir = if workdir.is_null() {
675 None
676 } else {
677 Some(DiffFile::from_raw(workdir))
678 };
679
680 let why = CheckoutNotificationType::from_bits_truncate(why as u32);
681 let keep_going = callback(why, path, baseline, target, workdir);
682 if keep_going {
683 0
684 } else {
685 1
686 }
687 })
688 .unwrap_or(2)
689 }
690
691 unsafe impl Send for TreeUpdateBuilder {}
692
693 impl Default for TreeUpdateBuilder {
694 fn default() -> Self {
695 Self::new()
696 }
697 }
698
699 impl TreeUpdateBuilder {
700 /// Create a new empty series of updates.
701 pub fn new() -> Self {
702 Self {
703 updates: Vec::new(),
704 paths: Vec::new(),
705 }
706 }
707
708 /// Add an update removing the specified `path` from a tree.
709 pub fn remove<T: IntoCString>(&mut self, path: T) -> &mut Self {
710 let path = util::cstring_to_repo_path(path).unwrap();
711 let path_ptr = path.as_ptr();
712 self.paths.push(path);
713 self.updates.push(raw::git_tree_update {
714 action: raw::GIT_TREE_UPDATE_REMOVE,
715 id: raw::git_oid {
716 id: [0; raw::GIT_OID_RAWSZ],
717 },
718 filemode: raw::GIT_FILEMODE_UNREADABLE,
719 path: path_ptr,
720 });
721 self
722 }
723
724 /// Add an update setting the specified `path` to a specific Oid, whether it currently exists
725 /// or not.
726 ///
727 /// Note that libgit2 does not support an upsert of a previously removed path, or an upsert
728 /// that changes the type of an object (such as from tree to blob or vice versa).
729 pub fn upsert<T: IntoCString>(&mut self, path: T, id: Oid, filemode: FileMode) -> &mut Self {
730 let path = util::cstring_to_repo_path(path).unwrap();
731 let path_ptr = path.as_ptr();
732 self.paths.push(path);
733 self.updates.push(raw::git_tree_update {
734 action: raw::GIT_TREE_UPDATE_UPSERT,
735 id: unsafe { *id.raw() },
736 filemode: u32::from(filemode) as raw::git_filemode_t,
737 path: path_ptr,
738 });
739 self
740 }
741
742 /// Create a new tree from the specified baseline and this series of updates.
743 ///
744 /// The baseline tree must exist in the specified repository.
745 pub fn create_updated(&mut self, repo: &Repository, baseline: &Tree<'_>) -> Result<Oid, Error> {
746 let mut ret = raw::git_oid {
747 id: [0; raw::GIT_OID_RAWSZ],
748 };
749 unsafe {
750 try_call!(raw::git_tree_create_updated(
751 &mut ret,
752 repo.raw(),
753 baseline.raw(),
754 self.updates.len(),
755 self.updates.as_ptr()
756 ));
757 Ok(Binding::from_raw(&ret as *const _))
758 }
759 }
760 }
761
762 #[cfg(test)]
763 mod tests {
764 use super::{CheckoutBuilder, RepoBuilder, TreeUpdateBuilder};
765 use crate::{CheckoutNotificationType, FileMode, Repository};
766 use std::fs;
767 use std::path::Path;
768 use tempfile::TempDir;
769
770 #[test]
771 fn smoke() {
772 let r = RepoBuilder::new().clone("/path/to/nowhere", Path::new("foo"));
773 assert!(r.is_err());
774 }
775
776 #[test]
777 fn smoke2() {
778 let td = TempDir::new().unwrap();
779 Repository::init_bare(&td.path().join("bare")).unwrap();
780 let url = if cfg!(unix) {
781 format!("file://{}/bare", td.path().display())
782 } else {
783 format!(
784 "file:///{}/bare",
785 td.path().display().to_string().replace("\\", "/")
786 )
787 };
788
789 let dst = td.path().join("foo");
790 RepoBuilder::new().clone(&url, &dst).unwrap();
791 fs::remove_dir_all(&dst).unwrap();
792 assert!(RepoBuilder::new().branch("foo").clone(&url, &dst).is_err());
793 }
794
795 #[test]
796 fn smoke_tree_create_updated() {
797 let (_tempdir, repo) = crate::test::repo_init();
798 let (_, tree_id) = crate::test::commit(&repo);
799 let tree = t!(repo.find_tree(tree_id));
800 assert!(tree.get_name("bar").is_none());
801 let foo_id = tree.get_name("foo").unwrap().id();
802 let tree2_id = t!(TreeUpdateBuilder::new()
803 .remove("foo")
804 .upsert("bar/baz", foo_id, FileMode::Blob)
805 .create_updated(&repo, &tree));
806 let tree2 = t!(repo.find_tree(tree2_id));
807 assert!(tree2.get_name("foo").is_none());
808 let baz_id = tree2.get_path(Path::new("bar/baz")).unwrap().id();
809 assert_eq!(foo_id, baz_id);
810 }
811
812 /// Issue regression test #365
813 #[test]
814 fn notify_callback() {
815 let td = TempDir::new().unwrap();
816 let cd = TempDir::new().unwrap();
817
818 {
819 let mut opts = crate::RepositoryInitOptions::new();
820 opts.initial_head("main");
821 let repo = Repository::init_opts(&td.path(), &opts).unwrap();
822
823 let mut config = repo.config().unwrap();
824 config.set_str("user.name", "name").unwrap();
825 config.set_str("user.email", "email").unwrap();
826
827 let mut index = repo.index().unwrap();
828 let p = Path::new(td.path()).join("file");
829 println!("using path {:?}", p);
830 fs::File::create(&p).unwrap();
831 index.add_path(&Path::new("file")).unwrap();
832 let id = index.write_tree().unwrap();
833
834 let tree = repo.find_tree(id).unwrap();
835 let sig = repo.signature().unwrap();
836 repo.commit(Some("HEAD"), &sig, &sig, "initial", &tree, &[])
837 .unwrap();
838 }
839
840 let repo = Repository::open_bare(&td.path().join(".git")).unwrap();
841 let tree = repo
842 .revparse_single(&"main")
843 .unwrap()
844 .peel_to_tree()
845 .unwrap();
846 let mut index = repo.index().unwrap();
847 index.read_tree(&tree).unwrap();
848
849 let mut checkout_opts = CheckoutBuilder::new();
850 checkout_opts.target_dir(&cd.path());
851 checkout_opts.notify_on(CheckoutNotificationType::all());
852 checkout_opts.notify(|_notif, _path, baseline, target, workdir| {
853 assert!(baseline.is_none());
854 assert_eq!(target.unwrap().path(), Some(Path::new("file")));
855 assert!(workdir.is_none());
856 true
857 });
858 repo.checkout_index(Some(&mut index), Some(&mut checkout_opts))
859 .unwrap();
860 }
861 }