]>
Commit | Line | Data |
---|---|---|
0a29b90c FG |
1 | use libc; |
2 | use raw::git_strarray; | |
781aab86 | 3 | use std::iter::FusedIterator; |
0a29b90c FG |
4 | use std::marker; |
5 | use std::mem; | |
6 | use std::ops::Range; | |
7 | use std::ptr; | |
8 | use std::slice; | |
9 | use std::str; | |
10 | use std::{ffi::CString, os::raw::c_char}; | |
11 | ||
12 | use crate::string_array::StringArray; | |
13 | use crate::util::Binding; | |
14 | use crate::{call, raw, Buf, Direction, Error, FetchPrune, Oid, ProxyOptions, Refspec}; | |
15 | use crate::{AutotagOption, Progress, RemoteCallbacks, Repository}; | |
16 | ||
17 | /// A structure representing a [remote][1] of a git repository. | |
18 | /// | |
19 | /// [1]: http://git-scm.com/book/en/Git-Basics-Working-with-Remotes | |
20 | /// | |
21 | /// The lifetime is the lifetime of the repository that it is attached to. The | |
22 | /// remote is used to manage fetches and pushes as well as refspecs. | |
23 | pub struct Remote<'repo> { | |
24 | raw: *mut raw::git_remote, | |
25 | _marker: marker::PhantomData<&'repo Repository>, | |
26 | } | |
27 | ||
28 | /// An iterator over the refspecs that a remote contains. | |
29 | pub struct Refspecs<'remote> { | |
30 | range: Range<usize>, | |
31 | remote: &'remote Remote<'remote>, | |
32 | } | |
33 | ||
34 | /// Description of a reference advertised by a remote server, given out on calls | |
35 | /// to `list`. | |
36 | pub struct RemoteHead<'remote> { | |
37 | raw: *const raw::git_remote_head, | |
38 | _marker: marker::PhantomData<&'remote str>, | |
39 | } | |
40 | ||
41 | /// Options which can be specified to various fetch operations. | |
42 | pub struct FetchOptions<'cb> { | |
43 | callbacks: Option<RemoteCallbacks<'cb>>, | |
ed00b5ec | 44 | depth: i32, |
0a29b90c FG |
45 | proxy: Option<ProxyOptions<'cb>>, |
46 | prune: FetchPrune, | |
47 | update_fetchhead: bool, | |
48 | download_tags: AutotagOption, | |
49 | follow_redirects: RemoteRedirect, | |
50 | custom_headers: Vec<CString>, | |
51 | custom_headers_ptrs: Vec<*const c_char>, | |
52 | } | |
53 | ||
54 | /// Options to control the behavior of a git push. | |
55 | pub struct PushOptions<'cb> { | |
56 | callbacks: Option<RemoteCallbacks<'cb>>, | |
57 | proxy: Option<ProxyOptions<'cb>>, | |
58 | pb_parallelism: u32, | |
59 | follow_redirects: RemoteRedirect, | |
60 | custom_headers: Vec<CString>, | |
61 | custom_headers_ptrs: Vec<*const c_char>, | |
62 | } | |
63 | ||
64 | /// Holds callbacks for a connection to a `Remote`. Disconnects when dropped | |
65 | pub struct RemoteConnection<'repo, 'connection, 'cb> { | |
66 | _callbacks: Box<RemoteCallbacks<'cb>>, | |
67 | _proxy: ProxyOptions<'cb>, | |
68 | remote: &'connection mut Remote<'repo>, | |
69 | } | |
70 | ||
71 | /// Remote redirection settings; whether redirects to another host are | |
72 | /// permitted. | |
73 | /// | |
74 | /// By default, git will follow a redirect on the initial request | |
75 | /// (`/info/refs`), but not subsequent requests. | |
76 | pub enum RemoteRedirect { | |
77 | /// Do not follow any off-site redirects at any stage of the fetch or push. | |
78 | None, | |
79 | /// Allow off-site redirects only upon the initial request. This is the | |
80 | /// default. | |
81 | Initial, | |
82 | /// Allow redirects at any stage in the fetch or push. | |
83 | All, | |
84 | } | |
85 | ||
86 | pub fn remote_into_raw(remote: Remote<'_>) -> *mut raw::git_remote { | |
87 | let ret = remote.raw; | |
88 | mem::forget(remote); | |
89 | ret | |
90 | } | |
91 | ||
92 | impl<'repo> Remote<'repo> { | |
93 | /// Ensure the remote name is well-formed. | |
94 | pub fn is_valid_name(remote_name: &str) -> bool { | |
95 | crate::init(); | |
96 | let remote_name = CString::new(remote_name).unwrap(); | |
97 | let mut valid: libc::c_int = 0; | |
98 | unsafe { | |
99 | call::c_try(raw::git_remote_name_is_valid( | |
100 | &mut valid, | |
101 | remote_name.as_ptr(), | |
102 | )) | |
103 | .unwrap(); | |
104 | } | |
105 | valid == 1 | |
106 | } | |
107 | ||
108 | /// Create a detached remote | |
109 | /// | |
110 | /// Create a remote with the given URL in-memory. You can use this | |
111 | /// when you have a URL instead of a remote's name. | |
112 | /// Contrasted with an anonymous remote, a detached remote will not | |
113 | /// consider any repo configuration values. | |
114 | pub fn create_detached<S: Into<Vec<u8>>>(url: S) -> Result<Remote<'repo>, Error> { | |
115 | crate::init(); | |
116 | let mut ret = ptr::null_mut(); | |
117 | let url = CString::new(url)?; | |
118 | unsafe { | |
119 | try_call!(raw::git_remote_create_detached(&mut ret, url)); | |
120 | Ok(Binding::from_raw(ret)) | |
121 | } | |
122 | } | |
123 | ||
124 | /// Get the remote's name. | |
125 | /// | |
126 | /// Returns `None` if this remote has not yet been named or if the name is | |
127 | /// not valid utf-8 | |
128 | pub fn name(&self) -> Option<&str> { | |
129 | self.name_bytes().and_then(|s| str::from_utf8(s).ok()) | |
130 | } | |
131 | ||
132 | /// Get the remote's name, in bytes. | |
133 | /// | |
134 | /// Returns `None` if this remote has not yet been named | |
135 | pub fn name_bytes(&self) -> Option<&[u8]> { | |
136 | unsafe { crate::opt_bytes(self, raw::git_remote_name(&*self.raw)) } | |
137 | } | |
138 | ||
139 | /// Get the remote's URL. | |
140 | /// | |
141 | /// Returns `None` if the URL is not valid utf-8 | |
142 | pub fn url(&self) -> Option<&str> { | |
143 | str::from_utf8(self.url_bytes()).ok() | |
144 | } | |
145 | ||
146 | /// Get the remote's URL as a byte array. | |
147 | pub fn url_bytes(&self) -> &[u8] { | |
148 | unsafe { crate::opt_bytes(self, raw::git_remote_url(&*self.raw)).unwrap() } | |
149 | } | |
150 | ||
151 | /// Get the remote's pushurl. | |
152 | /// | |
153 | /// Returns `None` if the pushurl is not valid utf-8 | |
154 | pub fn pushurl(&self) -> Option<&str> { | |
155 | self.pushurl_bytes().and_then(|s| str::from_utf8(s).ok()) | |
156 | } | |
157 | ||
158 | /// Get the remote's pushurl as a byte array. | |
159 | pub fn pushurl_bytes(&self) -> Option<&[u8]> { | |
160 | unsafe { crate::opt_bytes(self, raw::git_remote_pushurl(&*self.raw)) } | |
161 | } | |
162 | ||
163 | /// Get the remote's default branch. | |
164 | /// | |
165 | /// The remote (or more exactly its transport) must have connected to the | |
166 | /// remote repository. This default branch is available as soon as the | |
167 | /// connection to the remote is initiated and it remains available after | |
168 | /// disconnecting. | |
169 | pub fn default_branch(&self) -> Result<Buf, Error> { | |
170 | unsafe { | |
171 | let buf = Buf::new(); | |
172 | try_call!(raw::git_remote_default_branch(buf.raw(), self.raw)); | |
173 | Ok(buf) | |
174 | } | |
175 | } | |
176 | ||
177 | /// Open a connection to a remote. | |
178 | pub fn connect(&mut self, dir: Direction) -> Result<(), Error> { | |
179 | // TODO: can callbacks be exposed safely? | |
180 | unsafe { | |
181 | try_call!(raw::git_remote_connect( | |
182 | self.raw, | |
183 | dir, | |
184 | ptr::null(), | |
185 | ptr::null(), | |
186 | ptr::null() | |
187 | )); | |
188 | } | |
189 | Ok(()) | |
190 | } | |
191 | ||
192 | /// Open a connection to a remote with callbacks and proxy settings | |
193 | /// | |
194 | /// Returns a `RemoteConnection` that will disconnect once dropped | |
195 | pub fn connect_auth<'connection, 'cb>( | |
196 | &'connection mut self, | |
197 | dir: Direction, | |
198 | cb: Option<RemoteCallbacks<'cb>>, | |
199 | proxy_options: Option<ProxyOptions<'cb>>, | |
200 | ) -> Result<RemoteConnection<'repo, 'connection, 'cb>, Error> { | |
201 | let cb = Box::new(cb.unwrap_or_else(RemoteCallbacks::new)); | |
202 | let proxy_options = proxy_options.unwrap_or_else(ProxyOptions::new); | |
203 | unsafe { | |
204 | try_call!(raw::git_remote_connect( | |
205 | self.raw, | |
206 | dir, | |
207 | &cb.raw(), | |
208 | &proxy_options.raw(), | |
209 | ptr::null() | |
210 | )); | |
211 | } | |
212 | ||
213 | Ok(RemoteConnection { | |
214 | _callbacks: cb, | |
215 | _proxy: proxy_options, | |
216 | remote: self, | |
217 | }) | |
218 | } | |
219 | ||
220 | /// Check whether the remote is connected | |
221 | pub fn connected(&mut self) -> bool { | |
222 | unsafe { raw::git_remote_connected(self.raw) == 1 } | |
223 | } | |
224 | ||
225 | /// Disconnect from the remote | |
226 | pub fn disconnect(&mut self) -> Result<(), Error> { | |
227 | unsafe { | |
228 | try_call!(raw::git_remote_disconnect(self.raw)); | |
229 | } | |
230 | Ok(()) | |
231 | } | |
232 | ||
233 | /// Download and index the packfile | |
234 | /// | |
235 | /// Connect to the remote if it hasn't been done yet, negotiate with the | |
236 | /// remote git which objects are missing, download and index the packfile. | |
237 | /// | |
238 | /// The .idx file will be created and both it and the packfile with be | |
239 | /// renamed to their final name. | |
240 | /// | |
241 | /// The `specs` argument is a list of refspecs to use for this negotiation | |
242 | /// and download. Use an empty array to use the base refspecs. | |
243 | pub fn download<Str: AsRef<str> + crate::IntoCString + Clone>( | |
244 | &mut self, | |
245 | specs: &[Str], | |
246 | opts: Option<&mut FetchOptions<'_>>, | |
247 | ) -> Result<(), Error> { | |
248 | let (_a, _b, arr) = crate::util::iter2cstrs(specs.iter())?; | |
249 | let raw = opts.map(|o| o.raw()); | |
250 | unsafe { | |
251 | try_call!(raw::git_remote_download(self.raw, &arr, raw.as_ref())); | |
252 | } | |
253 | Ok(()) | |
254 | } | |
255 | ||
256 | /// Cancel the operation | |
257 | /// | |
258 | /// At certain points in its operation, the network code checks whether the | |
259 | /// operation has been canceled and if so stops the operation. | |
260 | pub fn stop(&mut self) -> Result<(), Error> { | |
261 | unsafe { | |
262 | try_call!(raw::git_remote_stop(self.raw)); | |
263 | } | |
264 | Ok(()) | |
265 | } | |
266 | ||
267 | /// Get the number of refspecs for a remote | |
268 | pub fn refspecs(&self) -> Refspecs<'_> { | |
269 | let cnt = unsafe { raw::git_remote_refspec_count(&*self.raw) as usize }; | |
270 | Refspecs { | |
271 | range: 0..cnt, | |
272 | remote: self, | |
273 | } | |
274 | } | |
275 | ||
276 | /// Get the `nth` refspec from this remote. | |
277 | /// | |
278 | /// The `refspecs` iterator can be used to iterate over all refspecs. | |
279 | pub fn get_refspec(&self, i: usize) -> Option<Refspec<'repo>> { | |
280 | unsafe { | |
281 | let ptr = raw::git_remote_get_refspec(&*self.raw, i as libc::size_t); | |
282 | Binding::from_raw_opt(ptr) | |
283 | } | |
284 | } | |
285 | ||
286 | /// Download new data and update tips | |
287 | /// | |
288 | /// Convenience function to connect to a remote, download the data, | |
289 | /// disconnect and update the remote-tracking branches. | |
290 | /// | |
291 | /// # Examples | |
292 | /// | |
293 | /// Example of functionality similar to `git fetch origin/main`: | |
294 | /// | |
295 | /// ```no_run | |
296 | /// fn fetch_origin_main(repo: git2::Repository) -> Result<(), git2::Error> { | |
297 | /// repo.find_remote("origin")?.fetch(&["main"], None, None) | |
298 | /// } | |
299 | /// | |
300 | /// let repo = git2::Repository::discover("rust").unwrap(); | |
301 | /// fetch_origin_main(repo).unwrap(); | |
302 | /// ``` | |
303 | pub fn fetch<Str: AsRef<str> + crate::IntoCString + Clone>( | |
304 | &mut self, | |
305 | refspecs: &[Str], | |
306 | opts: Option<&mut FetchOptions<'_>>, | |
307 | reflog_msg: Option<&str>, | |
308 | ) -> Result<(), Error> { | |
309 | let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?; | |
310 | let msg = crate::opt_cstr(reflog_msg)?; | |
311 | let raw = opts.map(|o| o.raw()); | |
312 | unsafe { | |
313 | try_call!(raw::git_remote_fetch(self.raw, &arr, raw.as_ref(), msg)); | |
314 | } | |
315 | Ok(()) | |
316 | } | |
317 | ||
318 | /// Update the tips to the new state | |
319 | pub fn update_tips( | |
320 | &mut self, | |
321 | callbacks: Option<&mut RemoteCallbacks<'_>>, | |
322 | update_fetchhead: bool, | |
323 | download_tags: AutotagOption, | |
324 | msg: Option<&str>, | |
325 | ) -> Result<(), Error> { | |
326 | let msg = crate::opt_cstr(msg)?; | |
327 | let cbs = callbacks.map(|cb| cb.raw()); | |
328 | unsafe { | |
329 | try_call!(raw::git_remote_update_tips( | |
330 | self.raw, | |
331 | cbs.as_ref(), | |
332 | update_fetchhead, | |
333 | download_tags, | |
334 | msg | |
335 | )); | |
336 | } | |
337 | Ok(()) | |
338 | } | |
339 | ||
340 | /// Perform a push | |
341 | /// | |
342 | /// Perform all the steps for a push. If no refspecs are passed then the | |
343 | /// configured refspecs will be used. | |
344 | /// | |
345 | /// Note that you'll likely want to use `RemoteCallbacks` and set | |
346 | /// `push_update_reference` to test whether all the references were pushed | |
347 | /// successfully. | |
348 | pub fn push<Str: AsRef<str> + crate::IntoCString + Clone>( | |
349 | &mut self, | |
350 | refspecs: &[Str], | |
351 | opts: Option<&mut PushOptions<'_>>, | |
352 | ) -> Result<(), Error> { | |
353 | let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?; | |
354 | let raw = opts.map(|o| o.raw()); | |
355 | unsafe { | |
356 | try_call!(raw::git_remote_push(self.raw, &arr, raw.as_ref())); | |
357 | } | |
358 | Ok(()) | |
359 | } | |
360 | ||
361 | /// Get the statistics structure that is filled in by the fetch operation. | |
362 | pub fn stats(&self) -> Progress<'_> { | |
363 | unsafe { Binding::from_raw(raw::git_remote_stats(self.raw)) } | |
364 | } | |
365 | ||
366 | /// Get the remote repository's reference advertisement list. | |
367 | /// | |
368 | /// Get the list of references with which the server responds to a new | |
369 | /// connection. | |
370 | /// | |
371 | /// The remote (or more exactly its transport) must have connected to the | |
372 | /// remote repository. This list is available as soon as the connection to | |
373 | /// the remote is initiated and it remains available after disconnecting. | |
374 | pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> { | |
375 | let mut size = 0; | |
376 | let mut base = ptr::null_mut(); | |
377 | unsafe { | |
378 | try_call!(raw::git_remote_ls(&mut base, &mut size, self.raw)); | |
379 | assert_eq!( | |
380 | mem::size_of::<RemoteHead<'_>>(), | |
381 | mem::size_of::<*const raw::git_remote_head>() | |
382 | ); | |
383 | let slice = slice::from_raw_parts(base as *const _, size as usize); | |
384 | Ok(mem::transmute::< | |
385 | &[*const raw::git_remote_head], | |
386 | &[RemoteHead<'_>], | |
387 | >(slice)) | |
388 | } | |
389 | } | |
390 | ||
391 | /// Prune tracking refs that are no longer present on remote | |
392 | pub fn prune(&mut self, callbacks: Option<RemoteCallbacks<'_>>) -> Result<(), Error> { | |
393 | let cbs = Box::new(callbacks.unwrap_or_else(RemoteCallbacks::new)); | |
394 | unsafe { | |
395 | try_call!(raw::git_remote_prune(self.raw, &cbs.raw())); | |
396 | } | |
397 | Ok(()) | |
398 | } | |
399 | ||
400 | /// Get the remote's list of fetch refspecs | |
401 | pub fn fetch_refspecs(&self) -> Result<StringArray, Error> { | |
402 | unsafe { | |
403 | let mut raw: raw::git_strarray = mem::zeroed(); | |
404 | try_call!(raw::git_remote_get_fetch_refspecs(&mut raw, self.raw)); | |
405 | Ok(StringArray::from_raw(raw)) | |
406 | } | |
407 | } | |
408 | ||
409 | /// Get the remote's list of push refspecs | |
410 | pub fn push_refspecs(&self) -> Result<StringArray, Error> { | |
411 | unsafe { | |
412 | let mut raw: raw::git_strarray = mem::zeroed(); | |
413 | try_call!(raw::git_remote_get_push_refspecs(&mut raw, self.raw)); | |
414 | Ok(StringArray::from_raw(raw)) | |
415 | } | |
416 | } | |
417 | } | |
418 | ||
419 | impl<'repo> Clone for Remote<'repo> { | |
420 | fn clone(&self) -> Remote<'repo> { | |
421 | let mut ret = ptr::null_mut(); | |
422 | let rc = unsafe { call!(raw::git_remote_dup(&mut ret, self.raw)) }; | |
423 | assert_eq!(rc, 0); | |
424 | Remote { | |
425 | raw: ret, | |
426 | _marker: marker::PhantomData, | |
427 | } | |
428 | } | |
429 | } | |
430 | ||
431 | impl<'repo> Binding for Remote<'repo> { | |
432 | type Raw = *mut raw::git_remote; | |
433 | ||
434 | unsafe fn from_raw(raw: *mut raw::git_remote) -> Remote<'repo> { | |
435 | Remote { | |
436 | raw, | |
437 | _marker: marker::PhantomData, | |
438 | } | |
439 | } | |
440 | fn raw(&self) -> *mut raw::git_remote { | |
441 | self.raw | |
442 | } | |
443 | } | |
444 | ||
445 | impl<'repo> Drop for Remote<'repo> { | |
446 | fn drop(&mut self) { | |
447 | unsafe { raw::git_remote_free(self.raw) } | |
448 | } | |
449 | } | |
450 | ||
451 | impl<'repo> Iterator for Refspecs<'repo> { | |
452 | type Item = Refspec<'repo>; | |
453 | fn next(&mut self) -> Option<Refspec<'repo>> { | |
454 | self.range.next().and_then(|i| self.remote.get_refspec(i)) | |
455 | } | |
456 | fn size_hint(&self) -> (usize, Option<usize>) { | |
457 | self.range.size_hint() | |
458 | } | |
459 | } | |
460 | impl<'repo> DoubleEndedIterator for Refspecs<'repo> { | |
461 | fn next_back(&mut self) -> Option<Refspec<'repo>> { | |
462 | self.range | |
463 | .next_back() | |
464 | .and_then(|i| self.remote.get_refspec(i)) | |
465 | } | |
466 | } | |
781aab86 | 467 | impl<'repo> FusedIterator for Refspecs<'repo> {} |
0a29b90c FG |
468 | impl<'repo> ExactSizeIterator for Refspecs<'repo> {} |
469 | ||
470 | #[allow(missing_docs)] // not documented in libgit2 :( | |
471 | impl<'remote> RemoteHead<'remote> { | |
472 | /// Flag if this is available locally. | |
473 | pub fn is_local(&self) -> bool { | |
474 | unsafe { (*self.raw).local != 0 } | |
475 | } | |
476 | ||
477 | pub fn oid(&self) -> Oid { | |
478 | unsafe { Binding::from_raw(&(*self.raw).oid as *const _) } | |
479 | } | |
480 | pub fn loid(&self) -> Oid { | |
481 | unsafe { Binding::from_raw(&(*self.raw).loid as *const _) } | |
482 | } | |
483 | ||
484 | pub fn name(&self) -> &str { | |
485 | let b = unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() }; | |
486 | str::from_utf8(b).unwrap() | |
487 | } | |
488 | ||
489 | pub fn symref_target(&self) -> Option<&str> { | |
490 | let b = unsafe { crate::opt_bytes(self, (*self.raw).symref_target) }; | |
491 | b.map(|b| str::from_utf8(b).unwrap()) | |
492 | } | |
493 | } | |
494 | ||
495 | impl<'cb> Default for FetchOptions<'cb> { | |
496 | fn default() -> Self { | |
497 | Self::new() | |
498 | } | |
499 | } | |
500 | ||
501 | impl<'cb> FetchOptions<'cb> { | |
502 | /// Creates a new blank set of fetch options | |
503 | pub fn new() -> FetchOptions<'cb> { | |
504 | FetchOptions { | |
505 | callbacks: None, | |
506 | proxy: None, | |
507 | prune: FetchPrune::Unspecified, | |
508 | update_fetchhead: true, | |
509 | download_tags: AutotagOption::Unspecified, | |
510 | follow_redirects: RemoteRedirect::Initial, | |
511 | custom_headers: Vec::new(), | |
512 | custom_headers_ptrs: Vec::new(), | |
ed00b5ec | 513 | depth: 0, // Not limited depth |
0a29b90c FG |
514 | } |
515 | } | |
516 | ||
517 | /// Set the callbacks to use for the fetch operation. | |
518 | pub fn remote_callbacks(&mut self, cbs: RemoteCallbacks<'cb>) -> &mut Self { | |
519 | self.callbacks = Some(cbs); | |
520 | self | |
521 | } | |
522 | ||
523 | /// Set the proxy options to use for the fetch operation. | |
524 | pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self { | |
525 | self.proxy = Some(opts); | |
526 | self | |
527 | } | |
528 | ||
529 | /// Set whether to perform a prune after the fetch. | |
530 | pub fn prune(&mut self, prune: FetchPrune) -> &mut Self { | |
531 | self.prune = prune; | |
532 | self | |
533 | } | |
534 | ||
535 | /// Set whether to write the results to FETCH_HEAD. | |
536 | /// | |
537 | /// Defaults to `true`. | |
538 | pub fn update_fetchhead(&mut self, update: bool) -> &mut Self { | |
539 | self.update_fetchhead = update; | |
540 | self | |
541 | } | |
542 | ||
ed00b5ec FG |
543 | /// Set fetch depth, a value less or equal to 0 is interpreted as pull |
544 | /// everything (effectively the same as not declaring a limit depth). | |
545 | ||
546 | // FIXME(blyxyas): We currently don't have a test for shallow functions | |
547 | // because libgit2 doesn't support local shallow clones. | |
548 | // https://github.com/rust-lang/git2-rs/pull/979#issuecomment-1716299900 | |
549 | pub fn depth(&mut self, depth: i32) -> &mut Self { | |
550 | self.depth = depth.max(0); | |
551 | self | |
552 | } | |
553 | ||
0a29b90c FG |
554 | /// Set how to behave regarding tags on the remote, such as auto-downloading |
555 | /// tags for objects we're downloading or downloading all of them. | |
556 | /// | |
557 | /// The default is to auto-follow tags. | |
558 | pub fn download_tags(&mut self, opt: AutotagOption) -> &mut Self { | |
559 | self.download_tags = opt; | |
560 | self | |
561 | } | |
562 | ||
563 | /// Set remote redirection settings; whether redirects to another host are | |
564 | /// permitted. | |
565 | /// | |
566 | /// By default, git will follow a redirect on the initial request | |
567 | /// (`/info/refs`), but not subsequent requests. | |
568 | pub fn follow_redirects(&mut self, redirect: RemoteRedirect) -> &mut Self { | |
569 | self.follow_redirects = redirect; | |
570 | self | |
571 | } | |
572 | ||
573 | /// Set extra headers for this fetch operation. | |
574 | pub fn custom_headers(&mut self, custom_headers: &[&str]) -> &mut Self { | |
575 | self.custom_headers = custom_headers | |
576 | .iter() | |
577 | .map(|&s| CString::new(s).unwrap()) | |
578 | .collect(); | |
579 | self.custom_headers_ptrs = self.custom_headers.iter().map(|s| s.as_ptr()).collect(); | |
580 | self | |
581 | } | |
582 | } | |
583 | ||
584 | impl<'cb> Binding for FetchOptions<'cb> { | |
585 | type Raw = raw::git_fetch_options; | |
586 | ||
587 | unsafe fn from_raw(_raw: raw::git_fetch_options) -> FetchOptions<'cb> { | |
588 | panic!("unimplemented"); | |
589 | } | |
590 | fn raw(&self) -> raw::git_fetch_options { | |
591 | raw::git_fetch_options { | |
592 | version: 1, | |
593 | callbacks: self | |
594 | .callbacks | |
595 | .as_ref() | |
596 | .map(|m| m.raw()) | |
597 | .unwrap_or_else(|| RemoteCallbacks::new().raw()), | |
598 | proxy_opts: self | |
599 | .proxy | |
600 | .as_ref() | |
601 | .map(|m| m.raw()) | |
602 | .unwrap_or_else(|| ProxyOptions::new().raw()), | |
603 | prune: crate::call::convert(&self.prune), | |
604 | update_fetchhead: crate::call::convert(&self.update_fetchhead), | |
605 | download_tags: crate::call::convert(&self.download_tags), | |
ed00b5ec | 606 | depth: self.depth, |
0a29b90c FG |
607 | follow_redirects: self.follow_redirects.raw(), |
608 | custom_headers: git_strarray { | |
609 | count: self.custom_headers_ptrs.len(), | |
610 | strings: self.custom_headers_ptrs.as_ptr() as *mut _, | |
611 | }, | |
612 | } | |
613 | } | |
614 | } | |
615 | ||
616 | impl<'cb> Default for PushOptions<'cb> { | |
617 | fn default() -> Self { | |
618 | Self::new() | |
619 | } | |
620 | } | |
621 | ||
622 | impl<'cb> PushOptions<'cb> { | |
623 | /// Creates a new blank set of push options | |
624 | pub fn new() -> PushOptions<'cb> { | |
625 | PushOptions { | |
626 | callbacks: None, | |
627 | proxy: None, | |
628 | pb_parallelism: 1, | |
629 | follow_redirects: RemoteRedirect::Initial, | |
630 | custom_headers: Vec::new(), | |
631 | custom_headers_ptrs: Vec::new(), | |
632 | } | |
633 | } | |
634 | ||
635 | /// Set the callbacks to use for the push operation. | |
636 | pub fn remote_callbacks(&mut self, cbs: RemoteCallbacks<'cb>) -> &mut Self { | |
637 | self.callbacks = Some(cbs); | |
638 | self | |
639 | } | |
640 | ||
641 | /// Set the proxy options to use for the push operation. | |
642 | pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self { | |
643 | self.proxy = Some(opts); | |
644 | self | |
645 | } | |
646 | ||
647 | /// If the transport being used to push to the remote requires the creation | |
648 | /// of a pack file, this controls the number of worker threads used by the | |
649 | /// packbuilder when creating that pack file to be sent to the remote. | |
650 | /// | |
651 | /// if set to 0 the packbuilder will auto-detect the number of threads to | |
652 | /// create, and the default value is 1. | |
653 | pub fn packbuilder_parallelism(&mut self, parallel: u32) -> &mut Self { | |
654 | self.pb_parallelism = parallel; | |
655 | self | |
656 | } | |
657 | ||
658 | /// Set remote redirection settings; whether redirects to another host are | |
659 | /// permitted. | |
660 | /// | |
661 | /// By default, git will follow a redirect on the initial request | |
662 | /// (`/info/refs`), but not subsequent requests. | |
663 | pub fn follow_redirects(&mut self, redirect: RemoteRedirect) -> &mut Self { | |
664 | self.follow_redirects = redirect; | |
665 | self | |
666 | } | |
667 | ||
668 | /// Set extra headers for this push operation. | |
669 | pub fn custom_headers(&mut self, custom_headers: &[&str]) -> &mut Self { | |
670 | self.custom_headers = custom_headers | |
671 | .iter() | |
672 | .map(|&s| CString::new(s).unwrap()) | |
673 | .collect(); | |
674 | self.custom_headers_ptrs = self.custom_headers.iter().map(|s| s.as_ptr()).collect(); | |
675 | self | |
676 | } | |
677 | } | |
678 | ||
679 | impl<'cb> Binding for PushOptions<'cb> { | |
680 | type Raw = raw::git_push_options; | |
681 | ||
682 | unsafe fn from_raw(_raw: raw::git_push_options) -> PushOptions<'cb> { | |
683 | panic!("unimplemented"); | |
684 | } | |
685 | fn raw(&self) -> raw::git_push_options { | |
686 | raw::git_push_options { | |
687 | version: 1, | |
688 | callbacks: self | |
689 | .callbacks | |
690 | .as_ref() | |
691 | .map(|m| m.raw()) | |
692 | .unwrap_or_else(|| RemoteCallbacks::new().raw()), | |
693 | proxy_opts: self | |
694 | .proxy | |
695 | .as_ref() | |
696 | .map(|m| m.raw()) | |
697 | .unwrap_or_else(|| ProxyOptions::new().raw()), | |
698 | pb_parallelism: self.pb_parallelism as libc::c_uint, | |
699 | follow_redirects: self.follow_redirects.raw(), | |
700 | custom_headers: git_strarray { | |
701 | count: self.custom_headers_ptrs.len(), | |
702 | strings: self.custom_headers_ptrs.as_ptr() as *mut _, | |
703 | }, | |
704 | } | |
705 | } | |
706 | } | |
707 | ||
708 | impl<'repo, 'connection, 'cb> RemoteConnection<'repo, 'connection, 'cb> { | |
709 | /// Check whether the remote is (still) connected | |
710 | pub fn connected(&mut self) -> bool { | |
711 | self.remote.connected() | |
712 | } | |
713 | ||
714 | /// Get the remote repository's reference advertisement list. | |
715 | /// | |
716 | /// This list is available as soon as the connection to | |
717 | /// the remote is initiated and it remains available after disconnecting. | |
718 | pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> { | |
719 | self.remote.list() | |
720 | } | |
721 | ||
722 | /// Get the remote's default branch. | |
723 | /// | |
724 | /// This default branch is available as soon as the connection to the remote | |
725 | /// is initiated and it remains available after disconnecting. | |
726 | pub fn default_branch(&self) -> Result<Buf, Error> { | |
727 | self.remote.default_branch() | |
728 | } | |
729 | ||
730 | /// access remote bound to this connection | |
731 | pub fn remote(&mut self) -> &mut Remote<'repo> { | |
732 | self.remote | |
733 | } | |
734 | } | |
735 | ||
736 | impl<'repo, 'connection, 'cb> Drop for RemoteConnection<'repo, 'connection, 'cb> { | |
737 | fn drop(&mut self) { | |
738 | drop(self.remote.disconnect()); | |
739 | } | |
740 | } | |
741 | ||
742 | impl Default for RemoteRedirect { | |
743 | fn default() -> Self { | |
744 | RemoteRedirect::Initial | |
745 | } | |
746 | } | |
747 | ||
748 | impl RemoteRedirect { | |
749 | fn raw(&self) -> raw::git_remote_redirect_t { | |
750 | match self { | |
751 | RemoteRedirect::None => raw::GIT_REMOTE_REDIRECT_NONE, | |
752 | RemoteRedirect::Initial => raw::GIT_REMOTE_REDIRECT_INITIAL, | |
753 | RemoteRedirect::All => raw::GIT_REMOTE_REDIRECT_ALL, | |
754 | } | |
755 | } | |
756 | } | |
757 | ||
758 | #[cfg(test)] | |
759 | mod tests { | |
760 | use crate::{AutotagOption, PushOptions}; | |
761 | use crate::{Direction, FetchOptions, Remote, RemoteCallbacks, Repository}; | |
762 | use std::cell::Cell; | |
763 | use tempfile::TempDir; | |
764 | ||
765 | #[test] | |
766 | fn smoke() { | |
767 | let (td, repo) = crate::test::repo_init(); | |
768 | t!(repo.remote("origin", "/path/to/nowhere")); | |
769 | drop(repo); | |
770 | ||
771 | let repo = t!(Repository::init(td.path())); | |
772 | let mut origin = t!(repo.find_remote("origin")); | |
773 | assert_eq!(origin.name(), Some("origin")); | |
774 | assert_eq!(origin.url(), Some("/path/to/nowhere")); | |
775 | assert_eq!(origin.pushurl(), None); | |
776 | ||
777 | t!(repo.remote_set_url("origin", "/path/to/elsewhere")); | |
778 | t!(repo.remote_set_pushurl("origin", Some("/path/to/elsewhere"))); | |
779 | ||
780 | let stats = origin.stats(); | |
781 | assert_eq!(stats.total_objects(), 0); | |
782 | ||
783 | t!(origin.stop()); | |
784 | } | |
785 | ||
786 | #[test] | |
787 | fn create_remote() { | |
788 | let td = TempDir::new().unwrap(); | |
789 | let remote = td.path().join("remote"); | |
790 | Repository::init_bare(&remote).unwrap(); | |
791 | ||
792 | let (_td, repo) = crate::test::repo_init(); | |
793 | let url = if cfg!(unix) { | |
794 | format!("file://{}", remote.display()) | |
795 | } else { | |
796 | format!( | |
797 | "file:///{}", | |
798 | remote.display().to_string().replace("\\", "/") | |
799 | ) | |
800 | }; | |
801 | ||
802 | let mut origin = repo.remote("origin", &url).unwrap(); | |
803 | assert_eq!(origin.name(), Some("origin")); | |
804 | assert_eq!(origin.url(), Some(&url[..])); | |
805 | assert_eq!(origin.pushurl(), None); | |
806 | ||
807 | { | |
808 | let mut specs = origin.refspecs(); | |
809 | let spec = specs.next().unwrap(); | |
810 | assert!(specs.next().is_none()); | |
811 | assert_eq!(spec.str(), Some("+refs/heads/*:refs/remotes/origin/*")); | |
812 | assert_eq!(spec.dst(), Some("refs/remotes/origin/*")); | |
813 | assert_eq!(spec.src(), Some("refs/heads/*")); | |
814 | assert!(spec.is_force()); | |
815 | } | |
816 | assert!(origin.refspecs().next_back().is_some()); | |
817 | { | |
818 | let remotes = repo.remotes().unwrap(); | |
819 | assert_eq!(remotes.len(), 1); | |
820 | assert_eq!(remotes.get(0), Some("origin")); | |
821 | assert_eq!(remotes.iter().count(), 1); | |
822 | assert_eq!(remotes.iter().next().unwrap(), Some("origin")); | |
823 | } | |
824 | ||
825 | origin.connect(Direction::Push).unwrap(); | |
826 | assert!(origin.connected()); | |
827 | origin.disconnect().unwrap(); | |
828 | ||
829 | origin.connect(Direction::Fetch).unwrap(); | |
830 | assert!(origin.connected()); | |
831 | origin.download(&[] as &[&str], None).unwrap(); | |
832 | origin.disconnect().unwrap(); | |
833 | ||
834 | { | |
835 | let mut connection = origin.connect_auth(Direction::Push, None, None).unwrap(); | |
836 | assert!(connection.connected()); | |
837 | } | |
838 | assert!(!origin.connected()); | |
839 | ||
840 | { | |
841 | let mut connection = origin.connect_auth(Direction::Fetch, None, None).unwrap(); | |
842 | assert!(connection.connected()); | |
843 | } | |
844 | assert!(!origin.connected()); | |
845 | ||
846 | origin.fetch(&[] as &[&str], None, None).unwrap(); | |
847 | origin.fetch(&[] as &[&str], None, Some("foo")).unwrap(); | |
848 | origin | |
849 | .update_tips(None, true, AutotagOption::Unspecified, None) | |
850 | .unwrap(); | |
851 | origin | |
852 | .update_tips(None, true, AutotagOption::All, Some("foo")) | |
853 | .unwrap(); | |
854 | ||
855 | t!(repo.remote_add_fetch("origin", "foo")); | |
856 | t!(repo.remote_add_fetch("origin", "bar")); | |
857 | } | |
858 | ||
859 | #[test] | |
860 | fn rename_remote() { | |
861 | let (_td, repo) = crate::test::repo_init(); | |
862 | repo.remote("origin", "foo").unwrap(); | |
863 | drop(repo.remote_rename("origin", "foo")); | |
864 | drop(repo.remote_delete("foo")); | |
865 | } | |
866 | ||
867 | #[test] | |
868 | fn create_remote_anonymous() { | |
869 | let td = TempDir::new().unwrap(); | |
870 | let repo = Repository::init(td.path()).unwrap(); | |
871 | ||
872 | let origin = repo.remote_anonymous("/path/to/nowhere").unwrap(); | |
873 | assert_eq!(origin.name(), None); | |
874 | drop(origin.clone()); | |
875 | } | |
876 | ||
877 | #[test] | |
878 | fn is_valid_name() { | |
879 | assert!(Remote::is_valid_name("foobar")); | |
880 | assert!(!Remote::is_valid_name("\x01")); | |
881 | } | |
882 | ||
883 | #[test] | |
884 | #[should_panic] | |
885 | fn is_valid_name_for_invalid_remote() { | |
886 | Remote::is_valid_name("ab\012"); | |
887 | } | |
888 | ||
889 | #[test] | |
890 | fn transfer_cb() { | |
891 | let (td, _repo) = crate::test::repo_init(); | |
892 | let td2 = TempDir::new().unwrap(); | |
893 | let url = crate::test::path2url(&td.path()); | |
894 | ||
895 | let repo = Repository::init(td2.path()).unwrap(); | |
896 | let progress_hit = Cell::new(false); | |
897 | { | |
898 | let mut callbacks = RemoteCallbacks::new(); | |
899 | let mut origin = repo.remote("origin", &url).unwrap(); | |
900 | ||
901 | callbacks.transfer_progress(|_progress| { | |
902 | progress_hit.set(true); | |
903 | true | |
904 | }); | |
905 | origin | |
906 | .fetch( | |
907 | &[] as &[&str], | |
908 | Some(FetchOptions::new().remote_callbacks(callbacks)), | |
909 | None, | |
910 | ) | |
911 | .unwrap(); | |
912 | ||
913 | let list = t!(origin.list()); | |
914 | assert_eq!(list.len(), 2); | |
915 | assert_eq!(list[0].name(), "HEAD"); | |
916 | assert!(!list[0].is_local()); | |
917 | assert_eq!(list[1].name(), "refs/heads/main"); | |
918 | assert!(!list[1].is_local()); | |
919 | } | |
920 | assert!(progress_hit.get()); | |
921 | } | |
922 | ||
923 | /// This test is meant to assure that the callbacks provided to connect will not cause | |
924 | /// segfaults | |
925 | #[test] | |
926 | fn connect_list() { | |
927 | let (td, _repo) = crate::test::repo_init(); | |
928 | let td2 = TempDir::new().unwrap(); | |
929 | let url = crate::test::path2url(&td.path()); | |
930 | ||
931 | let repo = Repository::init(td2.path()).unwrap(); | |
932 | let mut callbacks = RemoteCallbacks::new(); | |
933 | callbacks.sideband_progress(|_progress| { | |
934 | // no-op | |
935 | true | |
936 | }); | |
937 | ||
938 | let mut origin = repo.remote("origin", &url).unwrap(); | |
939 | ||
940 | { | |
941 | let mut connection = origin | |
942 | .connect_auth(Direction::Fetch, Some(callbacks), None) | |
943 | .unwrap(); | |
944 | assert!(connection.connected()); | |
945 | ||
946 | let list = t!(connection.list()); | |
947 | assert_eq!(list.len(), 2); | |
948 | assert_eq!(list[0].name(), "HEAD"); | |
949 | assert!(!list[0].is_local()); | |
950 | assert_eq!(list[1].name(), "refs/heads/main"); | |
951 | assert!(!list[1].is_local()); | |
952 | } | |
953 | assert!(!origin.connected()); | |
954 | } | |
955 | ||
956 | #[test] | |
957 | fn push() { | |
958 | let (_td, repo) = crate::test::repo_init(); | |
959 | let td2 = TempDir::new().unwrap(); | |
960 | let td3 = TempDir::new().unwrap(); | |
961 | let url = crate::test::path2url(&td2.path()); | |
962 | ||
963 | let mut opts = crate::RepositoryInitOptions::new(); | |
964 | opts.bare(true); | |
965 | opts.initial_head("main"); | |
966 | Repository::init_opts(td2.path(), &opts).unwrap(); | |
967 | // git push | |
968 | let mut remote = repo.remote("origin", &url).unwrap(); | |
969 | let mut updated = false; | |
970 | { | |
971 | let mut callbacks = RemoteCallbacks::new(); | |
972 | callbacks.push_update_reference(|refname, status| { | |
973 | updated = true; | |
974 | assert_eq!(refname, "refs/heads/main"); | |
975 | assert_eq!(status, None); | |
976 | Ok(()) | |
977 | }); | |
978 | let mut options = PushOptions::new(); | |
979 | options.remote_callbacks(callbacks); | |
980 | remote | |
981 | .push(&["refs/heads/main"], Some(&mut options)) | |
982 | .unwrap(); | |
983 | } | |
984 | assert!(updated); | |
985 | ||
986 | let repo = Repository::clone(&url, td3.path()).unwrap(); | |
987 | let commit = repo.head().unwrap().target().unwrap(); | |
988 | let commit = repo.find_commit(commit).unwrap(); | |
989 | assert_eq!(commit.message(), Some("initial\n\nbody")); | |
990 | } | |
991 | ||
992 | #[test] | |
993 | fn prune() { | |
994 | let (td, remote_repo) = crate::test::repo_init(); | |
995 | let oid = remote_repo.head().unwrap().target().unwrap(); | |
996 | let commit = remote_repo.find_commit(oid).unwrap(); | |
997 | remote_repo.branch("stale", &commit, true).unwrap(); | |
998 | ||
999 | let td2 = TempDir::new().unwrap(); | |
1000 | let url = crate::test::path2url(&td.path()); | |
1001 | let repo = Repository::clone(&url, &td2).unwrap(); | |
1002 | ||
1003 | fn assert_branch_count(repo: &Repository, count: usize) { | |
1004 | assert_eq!( | |
1005 | repo.branches(Some(crate::BranchType::Remote)) | |
1006 | .unwrap() | |
1007 | .filter(|b| b.as_ref().unwrap().0.name().unwrap() == Some("origin/stale")) | |
1008 | .count(), | |
1009 | count, | |
1010 | ); | |
1011 | } | |
1012 | ||
1013 | assert_branch_count(&repo, 1); | |
1014 | ||
1015 | // delete `stale` branch on remote repo | |
1016 | let mut stale_branch = remote_repo | |
1017 | .find_branch("stale", crate::BranchType::Local) | |
1018 | .unwrap(); | |
1019 | stale_branch.delete().unwrap(); | |
1020 | ||
1021 | // prune | |
1022 | let mut remote = repo.find_remote("origin").unwrap(); | |
1023 | remote.connect(Direction::Push).unwrap(); | |
1024 | let mut callbacks = RemoteCallbacks::new(); | |
1025 | callbacks.update_tips(|refname, _a, b| { | |
1026 | assert_eq!(refname, "refs/remotes/origin/stale"); | |
1027 | assert!(b.is_zero()); | |
1028 | true | |
1029 | }); | |
1030 | remote.prune(Some(callbacks)).unwrap(); | |
1031 | assert_branch_count(&repo, 0); | |
1032 | } | |
1033 | ||
1034 | #[test] | |
1035 | fn push_negotiation() { | |
1036 | let (_td, repo) = crate::test::repo_init(); | |
1037 | let oid = repo.head().unwrap().target().unwrap(); | |
1038 | ||
1039 | let td2 = TempDir::new().unwrap(); | |
1040 | let url = crate::test::path2url(td2.path()); | |
1041 | let mut opts = crate::RepositoryInitOptions::new(); | |
1042 | opts.bare(true); | |
1043 | opts.initial_head("main"); | |
1044 | let remote_repo = Repository::init_opts(td2.path(), &opts).unwrap(); | |
1045 | ||
1046 | // reject pushing a branch | |
1047 | let mut remote = repo.remote("origin", &url).unwrap(); | |
1048 | let mut updated = false; | |
1049 | { | |
1050 | let mut callbacks = RemoteCallbacks::new(); | |
1051 | callbacks.push_negotiation(|updates| { | |
1052 | assert!(!updated); | |
1053 | updated = true; | |
1054 | assert_eq!(updates.len(), 1); | |
1055 | let u = &updates[0]; | |
1056 | assert_eq!(u.src_refname().unwrap(), "refs/heads/main"); | |
1057 | assert!(u.src().is_zero()); | |
1058 | assert_eq!(u.dst_refname().unwrap(), "refs/heads/main"); | |
1059 | assert_eq!(u.dst(), oid); | |
1060 | Err(crate::Error::from_str("rejected")) | |
1061 | }); | |
1062 | let mut options = PushOptions::new(); | |
1063 | options.remote_callbacks(callbacks); | |
1064 | assert!(remote | |
1065 | .push(&["refs/heads/main"], Some(&mut options)) | |
1066 | .is_err()); | |
1067 | } | |
1068 | assert!(updated); | |
1069 | assert_eq!(remote_repo.branches(None).unwrap().count(), 0); | |
1070 | ||
1071 | // push 3 branches | |
1072 | let commit = repo.find_commit(oid).unwrap(); | |
1073 | repo.branch("new1", &commit, true).unwrap(); | |
1074 | repo.branch("new2", &commit, true).unwrap(); | |
1075 | let mut flag = 0; | |
1076 | updated = false; | |
1077 | { | |
1078 | let mut callbacks = RemoteCallbacks::new(); | |
1079 | callbacks.push_negotiation(|updates| { | |
1080 | assert!(!updated); | |
1081 | updated = true; | |
1082 | assert_eq!(updates.len(), 3); | |
1083 | for u in updates { | |
1084 | assert!(u.src().is_zero()); | |
1085 | assert_eq!(u.dst(), oid); | |
1086 | let src_name = u.src_refname().unwrap(); | |
1087 | let dst_name = u.dst_refname().unwrap(); | |
1088 | match src_name { | |
1089 | "refs/heads/main" => { | |
1090 | assert_eq!(dst_name, src_name); | |
1091 | flag |= 1; | |
1092 | } | |
1093 | "refs/heads/new1" => { | |
1094 | assert_eq!(dst_name, "refs/heads/dev1"); | |
1095 | flag |= 2; | |
1096 | } | |
1097 | "refs/heads/new2" => { | |
1098 | assert_eq!(dst_name, "refs/heads/dev2"); | |
1099 | flag |= 4; | |
1100 | } | |
1101 | _ => panic!("unexpected refname: {}", src_name), | |
1102 | } | |
1103 | } | |
1104 | Ok(()) | |
1105 | }); | |
1106 | let mut options = PushOptions::new(); | |
1107 | options.remote_callbacks(callbacks); | |
1108 | remote | |
1109 | .push( | |
1110 | &[ | |
1111 | "refs/heads/main", | |
1112 | "refs/heads/new1:refs/heads/dev1", | |
1113 | "refs/heads/new2:refs/heads/dev2", | |
1114 | ], | |
1115 | Some(&mut options), | |
1116 | ) | |
1117 | .unwrap(); | |
1118 | } | |
1119 | assert!(updated); | |
1120 | assert_eq!(flag, 7); | |
1121 | assert_eq!(remote_repo.branches(None).unwrap().count(), 3); | |
1122 | } | |
1123 | } |