mirror of
https://git.proxmox.com/git/rustc
synced 2026-01-17 19:04:12 +00:00
1124 lines
37 KiB
Rust
1124 lines
37 KiB
Rust
use libc;
|
|
use raw::git_strarray;
|
|
use std::iter::FusedIterator;
|
|
use std::marker;
|
|
use std::mem;
|
|
use std::ops::Range;
|
|
use std::ptr;
|
|
use std::slice;
|
|
use std::str;
|
|
use std::{ffi::CString, os::raw::c_char};
|
|
|
|
use crate::string_array::StringArray;
|
|
use crate::util::Binding;
|
|
use crate::{call, raw, Buf, Direction, Error, FetchPrune, Oid, ProxyOptions, Refspec};
|
|
use crate::{AutotagOption, Progress, RemoteCallbacks, Repository};
|
|
|
|
/// A structure representing a [remote][1] of a git repository.
|
|
///
|
|
/// [1]: http://git-scm.com/book/en/Git-Basics-Working-with-Remotes
|
|
///
|
|
/// The lifetime is the lifetime of the repository that it is attached to. The
|
|
/// remote is used to manage fetches and pushes as well as refspecs.
|
|
pub struct Remote<'repo> {
|
|
raw: *mut raw::git_remote,
|
|
_marker: marker::PhantomData<&'repo Repository>,
|
|
}
|
|
|
|
/// An iterator over the refspecs that a remote contains.
|
|
pub struct Refspecs<'remote> {
|
|
range: Range<usize>,
|
|
remote: &'remote Remote<'remote>,
|
|
}
|
|
|
|
/// Description of a reference advertised by a remote server, given out on calls
|
|
/// to `list`.
|
|
pub struct RemoteHead<'remote> {
|
|
raw: *const raw::git_remote_head,
|
|
_marker: marker::PhantomData<&'remote str>,
|
|
}
|
|
|
|
/// Options which can be specified to various fetch operations.
|
|
pub struct FetchOptions<'cb> {
|
|
callbacks: Option<RemoteCallbacks<'cb>>,
|
|
depth: i32,
|
|
proxy: Option<ProxyOptions<'cb>>,
|
|
prune: FetchPrune,
|
|
update_fetchhead: bool,
|
|
download_tags: AutotagOption,
|
|
follow_redirects: RemoteRedirect,
|
|
custom_headers: Vec<CString>,
|
|
custom_headers_ptrs: Vec<*const c_char>,
|
|
}
|
|
|
|
/// Options to control the behavior of a git push.
|
|
pub struct PushOptions<'cb> {
|
|
callbacks: Option<RemoteCallbacks<'cb>>,
|
|
proxy: Option<ProxyOptions<'cb>>,
|
|
pb_parallelism: u32,
|
|
follow_redirects: RemoteRedirect,
|
|
custom_headers: Vec<CString>,
|
|
custom_headers_ptrs: Vec<*const c_char>,
|
|
}
|
|
|
|
/// Holds callbacks for a connection to a `Remote`. Disconnects when dropped
|
|
pub struct RemoteConnection<'repo, 'connection, 'cb> {
|
|
_callbacks: Box<RemoteCallbacks<'cb>>,
|
|
_proxy: ProxyOptions<'cb>,
|
|
remote: &'connection mut Remote<'repo>,
|
|
}
|
|
|
|
/// Remote redirection settings; whether redirects to another host are
|
|
/// permitted.
|
|
///
|
|
/// By default, git will follow a redirect on the initial request
|
|
/// (`/info/refs`), but not subsequent requests.
|
|
pub enum RemoteRedirect {
|
|
/// Do not follow any off-site redirects at any stage of the fetch or push.
|
|
None,
|
|
/// Allow off-site redirects only upon the initial request. This is the
|
|
/// default.
|
|
Initial,
|
|
/// Allow redirects at any stage in the fetch or push.
|
|
All,
|
|
}
|
|
|
|
pub fn remote_into_raw(remote: Remote<'_>) -> *mut raw::git_remote {
|
|
let ret = remote.raw;
|
|
mem::forget(remote);
|
|
ret
|
|
}
|
|
|
|
impl<'repo> Remote<'repo> {
|
|
/// Ensure the remote name is well-formed.
|
|
pub fn is_valid_name(remote_name: &str) -> bool {
|
|
crate::init();
|
|
let remote_name = CString::new(remote_name).unwrap();
|
|
let mut valid: libc::c_int = 0;
|
|
unsafe {
|
|
call::c_try(raw::git_remote_name_is_valid(
|
|
&mut valid,
|
|
remote_name.as_ptr(),
|
|
))
|
|
.unwrap();
|
|
}
|
|
valid == 1
|
|
}
|
|
|
|
/// Create a detached remote
|
|
///
|
|
/// Create a remote with the given URL in-memory. You can use this
|
|
/// when you have a URL instead of a remote's name.
|
|
/// Contrasted with an anonymous remote, a detached remote will not
|
|
/// consider any repo configuration values.
|
|
pub fn create_detached<S: Into<Vec<u8>>>(url: S) -> Result<Remote<'repo>, Error> {
|
|
crate::init();
|
|
let mut ret = ptr::null_mut();
|
|
let url = CString::new(url)?;
|
|
unsafe {
|
|
try_call!(raw::git_remote_create_detached(&mut ret, url));
|
|
Ok(Binding::from_raw(ret))
|
|
}
|
|
}
|
|
|
|
/// Get the remote's name.
|
|
///
|
|
/// Returns `None` if this remote has not yet been named or if the name is
|
|
/// not valid utf-8
|
|
pub fn name(&self) -> Option<&str> {
|
|
self.name_bytes().and_then(|s| str::from_utf8(s).ok())
|
|
}
|
|
|
|
/// Get the remote's name, in bytes.
|
|
///
|
|
/// Returns `None` if this remote has not yet been named
|
|
pub fn name_bytes(&self) -> Option<&[u8]> {
|
|
unsafe { crate::opt_bytes(self, raw::git_remote_name(&*self.raw)) }
|
|
}
|
|
|
|
/// Get the remote's URL.
|
|
///
|
|
/// Returns `None` if the URL is not valid utf-8
|
|
pub fn url(&self) -> Option<&str> {
|
|
str::from_utf8(self.url_bytes()).ok()
|
|
}
|
|
|
|
/// Get the remote's URL as a byte array.
|
|
pub fn url_bytes(&self) -> &[u8] {
|
|
unsafe { crate::opt_bytes(self, raw::git_remote_url(&*self.raw)).unwrap() }
|
|
}
|
|
|
|
/// Get the remote's pushurl.
|
|
///
|
|
/// Returns `None` if the pushurl is not valid utf-8
|
|
pub fn pushurl(&self) -> Option<&str> {
|
|
self.pushurl_bytes().and_then(|s| str::from_utf8(s).ok())
|
|
}
|
|
|
|
/// Get the remote's pushurl as a byte array.
|
|
pub fn pushurl_bytes(&self) -> Option<&[u8]> {
|
|
unsafe { crate::opt_bytes(self, raw::git_remote_pushurl(&*self.raw)) }
|
|
}
|
|
|
|
/// Get the remote's default branch.
|
|
///
|
|
/// The remote (or more exactly its transport) must have connected to the
|
|
/// remote repository. This default branch is available as soon as the
|
|
/// connection to the remote is initiated and it remains available after
|
|
/// disconnecting.
|
|
pub fn default_branch(&self) -> Result<Buf, Error> {
|
|
unsafe {
|
|
let buf = Buf::new();
|
|
try_call!(raw::git_remote_default_branch(buf.raw(), self.raw));
|
|
Ok(buf)
|
|
}
|
|
}
|
|
|
|
/// Open a connection to a remote.
|
|
pub fn connect(&mut self, dir: Direction) -> Result<(), Error> {
|
|
// TODO: can callbacks be exposed safely?
|
|
unsafe {
|
|
try_call!(raw::git_remote_connect(
|
|
self.raw,
|
|
dir,
|
|
ptr::null(),
|
|
ptr::null(),
|
|
ptr::null()
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Open a connection to a remote with callbacks and proxy settings
|
|
///
|
|
/// Returns a `RemoteConnection` that will disconnect once dropped
|
|
pub fn connect_auth<'connection, 'cb>(
|
|
&'connection mut self,
|
|
dir: Direction,
|
|
cb: Option<RemoteCallbacks<'cb>>,
|
|
proxy_options: Option<ProxyOptions<'cb>>,
|
|
) -> Result<RemoteConnection<'repo, 'connection, 'cb>, Error> {
|
|
let cb = Box::new(cb.unwrap_or_else(RemoteCallbacks::new));
|
|
let proxy_options = proxy_options.unwrap_or_else(ProxyOptions::new);
|
|
unsafe {
|
|
try_call!(raw::git_remote_connect(
|
|
self.raw,
|
|
dir,
|
|
&cb.raw(),
|
|
&proxy_options.raw(),
|
|
ptr::null()
|
|
));
|
|
}
|
|
|
|
Ok(RemoteConnection {
|
|
_callbacks: cb,
|
|
_proxy: proxy_options,
|
|
remote: self,
|
|
})
|
|
}
|
|
|
|
/// Check whether the remote is connected
|
|
pub fn connected(&mut self) -> bool {
|
|
unsafe { raw::git_remote_connected(self.raw) == 1 }
|
|
}
|
|
|
|
/// Disconnect from the remote
|
|
pub fn disconnect(&mut self) -> Result<(), Error> {
|
|
unsafe {
|
|
try_call!(raw::git_remote_disconnect(self.raw));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Download and index the packfile
|
|
///
|
|
/// Connect to the remote if it hasn't been done yet, negotiate with the
|
|
/// remote git which objects are missing, download and index the packfile.
|
|
///
|
|
/// The .idx file will be created and both it and the packfile with be
|
|
/// renamed to their final name.
|
|
///
|
|
/// The `specs` argument is a list of refspecs to use for this negotiation
|
|
/// and download. Use an empty array to use the base refspecs.
|
|
pub fn download<Str: AsRef<str> + crate::IntoCString + Clone>(
|
|
&mut self,
|
|
specs: &[Str],
|
|
opts: Option<&mut FetchOptions<'_>>,
|
|
) -> Result<(), Error> {
|
|
let (_a, _b, arr) = crate::util::iter2cstrs(specs.iter())?;
|
|
let raw = opts.map(|o| o.raw());
|
|
unsafe {
|
|
try_call!(raw::git_remote_download(self.raw, &arr, raw.as_ref()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Cancel the operation
|
|
///
|
|
/// At certain points in its operation, the network code checks whether the
|
|
/// operation has been canceled and if so stops the operation.
|
|
pub fn stop(&mut self) -> Result<(), Error> {
|
|
unsafe {
|
|
try_call!(raw::git_remote_stop(self.raw));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the number of refspecs for a remote
|
|
pub fn refspecs(&self) -> Refspecs<'_> {
|
|
let cnt = unsafe { raw::git_remote_refspec_count(&*self.raw) as usize };
|
|
Refspecs {
|
|
range: 0..cnt,
|
|
remote: self,
|
|
}
|
|
}
|
|
|
|
/// Get the `nth` refspec from this remote.
|
|
///
|
|
/// The `refspecs` iterator can be used to iterate over all refspecs.
|
|
pub fn get_refspec(&self, i: usize) -> Option<Refspec<'repo>> {
|
|
unsafe {
|
|
let ptr = raw::git_remote_get_refspec(&*self.raw, i as libc::size_t);
|
|
Binding::from_raw_opt(ptr)
|
|
}
|
|
}
|
|
|
|
/// Download new data and update tips
|
|
///
|
|
/// Convenience function to connect to a remote, download the data,
|
|
/// disconnect and update the remote-tracking branches.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// Example of functionality similar to `git fetch origin/main`:
|
|
///
|
|
/// ```no_run
|
|
/// fn fetch_origin_main(repo: git2::Repository) -> Result<(), git2::Error> {
|
|
/// repo.find_remote("origin")?.fetch(&["main"], None, None)
|
|
/// }
|
|
///
|
|
/// let repo = git2::Repository::discover("rust").unwrap();
|
|
/// fetch_origin_main(repo).unwrap();
|
|
/// ```
|
|
pub fn fetch<Str: AsRef<str> + crate::IntoCString + Clone>(
|
|
&mut self,
|
|
refspecs: &[Str],
|
|
opts: Option<&mut FetchOptions<'_>>,
|
|
reflog_msg: Option<&str>,
|
|
) -> Result<(), Error> {
|
|
let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?;
|
|
let msg = crate::opt_cstr(reflog_msg)?;
|
|
let raw = opts.map(|o| o.raw());
|
|
unsafe {
|
|
try_call!(raw::git_remote_fetch(self.raw, &arr, raw.as_ref(), msg));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Update the tips to the new state
|
|
pub fn update_tips(
|
|
&mut self,
|
|
callbacks: Option<&mut RemoteCallbacks<'_>>,
|
|
update_fetchhead: bool,
|
|
download_tags: AutotagOption,
|
|
msg: Option<&str>,
|
|
) -> Result<(), Error> {
|
|
let msg = crate::opt_cstr(msg)?;
|
|
let cbs = callbacks.map(|cb| cb.raw());
|
|
unsafe {
|
|
try_call!(raw::git_remote_update_tips(
|
|
self.raw,
|
|
cbs.as_ref(),
|
|
update_fetchhead,
|
|
download_tags,
|
|
msg
|
|
));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Perform a push
|
|
///
|
|
/// Perform all the steps for a push. If no refspecs are passed then the
|
|
/// configured refspecs will be used.
|
|
///
|
|
/// Note that you'll likely want to use `RemoteCallbacks` and set
|
|
/// `push_update_reference` to test whether all the references were pushed
|
|
/// successfully.
|
|
pub fn push<Str: AsRef<str> + crate::IntoCString + Clone>(
|
|
&mut self,
|
|
refspecs: &[Str],
|
|
opts: Option<&mut PushOptions<'_>>,
|
|
) -> Result<(), Error> {
|
|
let (_a, _b, arr) = crate::util::iter2cstrs(refspecs.iter())?;
|
|
let raw = opts.map(|o| o.raw());
|
|
unsafe {
|
|
try_call!(raw::git_remote_push(self.raw, &arr, raw.as_ref()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the statistics structure that is filled in by the fetch operation.
|
|
pub fn stats(&self) -> Progress<'_> {
|
|
unsafe { Binding::from_raw(raw::git_remote_stats(self.raw)) }
|
|
}
|
|
|
|
/// Get the remote repository's reference advertisement list.
|
|
///
|
|
/// Get the list of references with which the server responds to a new
|
|
/// connection.
|
|
///
|
|
/// The remote (or more exactly its transport) must have connected to the
|
|
/// remote repository. This list is available as soon as the connection to
|
|
/// the remote is initiated and it remains available after disconnecting.
|
|
pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> {
|
|
let mut size = 0;
|
|
let mut base = ptr::null_mut();
|
|
unsafe {
|
|
try_call!(raw::git_remote_ls(&mut base, &mut size, self.raw));
|
|
assert_eq!(
|
|
mem::size_of::<RemoteHead<'_>>(),
|
|
mem::size_of::<*const raw::git_remote_head>()
|
|
);
|
|
let slice = slice::from_raw_parts(base as *const _, size as usize);
|
|
Ok(mem::transmute::<
|
|
&[*const raw::git_remote_head],
|
|
&[RemoteHead<'_>],
|
|
>(slice))
|
|
}
|
|
}
|
|
|
|
/// Prune tracking refs that are no longer present on remote
|
|
pub fn prune(&mut self, callbacks: Option<RemoteCallbacks<'_>>) -> Result<(), Error> {
|
|
let cbs = Box::new(callbacks.unwrap_or_else(RemoteCallbacks::new));
|
|
unsafe {
|
|
try_call!(raw::git_remote_prune(self.raw, &cbs.raw()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Get the remote's list of fetch refspecs
|
|
pub fn fetch_refspecs(&self) -> Result<StringArray, Error> {
|
|
unsafe {
|
|
let mut raw: raw::git_strarray = mem::zeroed();
|
|
try_call!(raw::git_remote_get_fetch_refspecs(&mut raw, self.raw));
|
|
Ok(StringArray::from_raw(raw))
|
|
}
|
|
}
|
|
|
|
/// Get the remote's list of push refspecs
|
|
pub fn push_refspecs(&self) -> Result<StringArray, Error> {
|
|
unsafe {
|
|
let mut raw: raw::git_strarray = mem::zeroed();
|
|
try_call!(raw::git_remote_get_push_refspecs(&mut raw, self.raw));
|
|
Ok(StringArray::from_raw(raw))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'repo> Clone for Remote<'repo> {
|
|
fn clone(&self) -> Remote<'repo> {
|
|
let mut ret = ptr::null_mut();
|
|
let rc = unsafe { call!(raw::git_remote_dup(&mut ret, self.raw)) };
|
|
assert_eq!(rc, 0);
|
|
Remote {
|
|
raw: ret,
|
|
_marker: marker::PhantomData,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'repo> Binding for Remote<'repo> {
|
|
type Raw = *mut raw::git_remote;
|
|
|
|
unsafe fn from_raw(raw: *mut raw::git_remote) -> Remote<'repo> {
|
|
Remote {
|
|
raw,
|
|
_marker: marker::PhantomData,
|
|
}
|
|
}
|
|
fn raw(&self) -> *mut raw::git_remote {
|
|
self.raw
|
|
}
|
|
}
|
|
|
|
impl<'repo> Drop for Remote<'repo> {
|
|
fn drop(&mut self) {
|
|
unsafe { raw::git_remote_free(self.raw) }
|
|
}
|
|
}
|
|
|
|
impl<'repo> Iterator for Refspecs<'repo> {
|
|
type Item = Refspec<'repo>;
|
|
fn next(&mut self) -> Option<Refspec<'repo>> {
|
|
self.range.next().and_then(|i| self.remote.get_refspec(i))
|
|
}
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
self.range.size_hint()
|
|
}
|
|
}
|
|
impl<'repo> DoubleEndedIterator for Refspecs<'repo> {
|
|
fn next_back(&mut self) -> Option<Refspec<'repo>> {
|
|
self.range
|
|
.next_back()
|
|
.and_then(|i| self.remote.get_refspec(i))
|
|
}
|
|
}
|
|
impl<'repo> FusedIterator for Refspecs<'repo> {}
|
|
impl<'repo> ExactSizeIterator for Refspecs<'repo> {}
|
|
|
|
#[allow(missing_docs)] // not documented in libgit2 :(
|
|
impl<'remote> RemoteHead<'remote> {
|
|
/// Flag if this is available locally.
|
|
pub fn is_local(&self) -> bool {
|
|
unsafe { (*self.raw).local != 0 }
|
|
}
|
|
|
|
pub fn oid(&self) -> Oid {
|
|
unsafe { Binding::from_raw(&(*self.raw).oid as *const _) }
|
|
}
|
|
pub fn loid(&self) -> Oid {
|
|
unsafe { Binding::from_raw(&(*self.raw).loid as *const _) }
|
|
}
|
|
|
|
pub fn name(&self) -> &str {
|
|
let b = unsafe { crate::opt_bytes(self, (*self.raw).name).unwrap() };
|
|
str::from_utf8(b).unwrap()
|
|
}
|
|
|
|
pub fn symref_target(&self) -> Option<&str> {
|
|
let b = unsafe { crate::opt_bytes(self, (*self.raw).symref_target) };
|
|
b.map(|b| str::from_utf8(b).unwrap())
|
|
}
|
|
}
|
|
|
|
impl<'cb> Default for FetchOptions<'cb> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<'cb> FetchOptions<'cb> {
|
|
/// Creates a new blank set of fetch options
|
|
pub fn new() -> FetchOptions<'cb> {
|
|
FetchOptions {
|
|
callbacks: None,
|
|
proxy: None,
|
|
prune: FetchPrune::Unspecified,
|
|
update_fetchhead: true,
|
|
download_tags: AutotagOption::Unspecified,
|
|
follow_redirects: RemoteRedirect::Initial,
|
|
custom_headers: Vec::new(),
|
|
custom_headers_ptrs: Vec::new(),
|
|
depth: 0, // Not limited depth
|
|
}
|
|
}
|
|
|
|
/// Set the callbacks to use for the fetch operation.
|
|
pub fn remote_callbacks(&mut self, cbs: RemoteCallbacks<'cb>) -> &mut Self {
|
|
self.callbacks = Some(cbs);
|
|
self
|
|
}
|
|
|
|
/// Set the proxy options to use for the fetch operation.
|
|
pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self {
|
|
self.proxy = Some(opts);
|
|
self
|
|
}
|
|
|
|
/// Set whether to perform a prune after the fetch.
|
|
pub fn prune(&mut self, prune: FetchPrune) -> &mut Self {
|
|
self.prune = prune;
|
|
self
|
|
}
|
|
|
|
/// Set whether to write the results to FETCH_HEAD.
|
|
///
|
|
/// Defaults to `true`.
|
|
pub fn update_fetchhead(&mut self, update: bool) -> &mut Self {
|
|
self.update_fetchhead = update;
|
|
self
|
|
}
|
|
|
|
/// Set fetch depth, a value less or equal to 0 is interpreted as pull
|
|
/// everything (effectively the same as not declaring a limit depth).
|
|
|
|
// FIXME(blyxyas): We currently don't have a test for shallow functions
|
|
// because libgit2 doesn't support local shallow clones.
|
|
// https://github.com/rust-lang/git2-rs/pull/979#issuecomment-1716299900
|
|
pub fn depth(&mut self, depth: i32) -> &mut Self {
|
|
self.depth = depth.max(0);
|
|
self
|
|
}
|
|
|
|
/// Set how to behave regarding tags on the remote, such as auto-downloading
|
|
/// tags for objects we're downloading or downloading all of them.
|
|
///
|
|
/// The default is to auto-follow tags.
|
|
pub fn download_tags(&mut self, opt: AutotagOption) -> &mut Self {
|
|
self.download_tags = opt;
|
|
self
|
|
}
|
|
|
|
/// Set remote redirection settings; whether redirects to another host are
|
|
/// permitted.
|
|
///
|
|
/// By default, git will follow a redirect on the initial request
|
|
/// (`/info/refs`), but not subsequent requests.
|
|
pub fn follow_redirects(&mut self, redirect: RemoteRedirect) -> &mut Self {
|
|
self.follow_redirects = redirect;
|
|
self
|
|
}
|
|
|
|
/// Set extra headers for this fetch operation.
|
|
pub fn custom_headers(&mut self, custom_headers: &[&str]) -> &mut Self {
|
|
self.custom_headers = custom_headers
|
|
.iter()
|
|
.map(|&s| CString::new(s).unwrap())
|
|
.collect();
|
|
self.custom_headers_ptrs = self.custom_headers.iter().map(|s| s.as_ptr()).collect();
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<'cb> Binding for FetchOptions<'cb> {
|
|
type Raw = raw::git_fetch_options;
|
|
|
|
unsafe fn from_raw(_raw: raw::git_fetch_options) -> FetchOptions<'cb> {
|
|
panic!("unimplemented");
|
|
}
|
|
fn raw(&self) -> raw::git_fetch_options {
|
|
raw::git_fetch_options {
|
|
version: 1,
|
|
callbacks: self
|
|
.callbacks
|
|
.as_ref()
|
|
.map(|m| m.raw())
|
|
.unwrap_or_else(|| RemoteCallbacks::new().raw()),
|
|
proxy_opts: self
|
|
.proxy
|
|
.as_ref()
|
|
.map(|m| m.raw())
|
|
.unwrap_or_else(|| ProxyOptions::new().raw()),
|
|
prune: crate::call::convert(&self.prune),
|
|
update_fetchhead: crate::call::convert(&self.update_fetchhead),
|
|
download_tags: crate::call::convert(&self.download_tags),
|
|
depth: self.depth,
|
|
follow_redirects: self.follow_redirects.raw(),
|
|
custom_headers: git_strarray {
|
|
count: self.custom_headers_ptrs.len(),
|
|
strings: self.custom_headers_ptrs.as_ptr() as *mut _,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'cb> Default for PushOptions<'cb> {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl<'cb> PushOptions<'cb> {
|
|
/// Creates a new blank set of push options
|
|
pub fn new() -> PushOptions<'cb> {
|
|
PushOptions {
|
|
callbacks: None,
|
|
proxy: None,
|
|
pb_parallelism: 1,
|
|
follow_redirects: RemoteRedirect::Initial,
|
|
custom_headers: Vec::new(),
|
|
custom_headers_ptrs: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Set the callbacks to use for the push operation.
|
|
pub fn remote_callbacks(&mut self, cbs: RemoteCallbacks<'cb>) -> &mut Self {
|
|
self.callbacks = Some(cbs);
|
|
self
|
|
}
|
|
|
|
/// Set the proxy options to use for the push operation.
|
|
pub fn proxy_options(&mut self, opts: ProxyOptions<'cb>) -> &mut Self {
|
|
self.proxy = Some(opts);
|
|
self
|
|
}
|
|
|
|
/// If the transport being used to push to the remote requires the creation
|
|
/// of a pack file, this controls the number of worker threads used by the
|
|
/// packbuilder when creating that pack file to be sent to the remote.
|
|
///
|
|
/// if set to 0 the packbuilder will auto-detect the number of threads to
|
|
/// create, and the default value is 1.
|
|
pub fn packbuilder_parallelism(&mut self, parallel: u32) -> &mut Self {
|
|
self.pb_parallelism = parallel;
|
|
self
|
|
}
|
|
|
|
/// Set remote redirection settings; whether redirects to another host are
|
|
/// permitted.
|
|
///
|
|
/// By default, git will follow a redirect on the initial request
|
|
/// (`/info/refs`), but not subsequent requests.
|
|
pub fn follow_redirects(&mut self, redirect: RemoteRedirect) -> &mut Self {
|
|
self.follow_redirects = redirect;
|
|
self
|
|
}
|
|
|
|
/// Set extra headers for this push operation.
|
|
pub fn custom_headers(&mut self, custom_headers: &[&str]) -> &mut Self {
|
|
self.custom_headers = custom_headers
|
|
.iter()
|
|
.map(|&s| CString::new(s).unwrap())
|
|
.collect();
|
|
self.custom_headers_ptrs = self.custom_headers.iter().map(|s| s.as_ptr()).collect();
|
|
self
|
|
}
|
|
}
|
|
|
|
impl<'cb> Binding for PushOptions<'cb> {
|
|
type Raw = raw::git_push_options;
|
|
|
|
unsafe fn from_raw(_raw: raw::git_push_options) -> PushOptions<'cb> {
|
|
panic!("unimplemented");
|
|
}
|
|
fn raw(&self) -> raw::git_push_options {
|
|
raw::git_push_options {
|
|
version: 1,
|
|
callbacks: self
|
|
.callbacks
|
|
.as_ref()
|
|
.map(|m| m.raw())
|
|
.unwrap_or_else(|| RemoteCallbacks::new().raw()),
|
|
proxy_opts: self
|
|
.proxy
|
|
.as_ref()
|
|
.map(|m| m.raw())
|
|
.unwrap_or_else(|| ProxyOptions::new().raw()),
|
|
pb_parallelism: self.pb_parallelism as libc::c_uint,
|
|
follow_redirects: self.follow_redirects.raw(),
|
|
custom_headers: git_strarray {
|
|
count: self.custom_headers_ptrs.len(),
|
|
strings: self.custom_headers_ptrs.as_ptr() as *mut _,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'repo, 'connection, 'cb> RemoteConnection<'repo, 'connection, 'cb> {
|
|
/// Check whether the remote is (still) connected
|
|
pub fn connected(&mut self) -> bool {
|
|
self.remote.connected()
|
|
}
|
|
|
|
/// Get the remote repository's reference advertisement list.
|
|
///
|
|
/// This list is available as soon as the connection to
|
|
/// the remote is initiated and it remains available after disconnecting.
|
|
pub fn list(&self) -> Result<&[RemoteHead<'_>], Error> {
|
|
self.remote.list()
|
|
}
|
|
|
|
/// Get the remote's default branch.
|
|
///
|
|
/// This default branch is available as soon as the connection to the remote
|
|
/// is initiated and it remains available after disconnecting.
|
|
pub fn default_branch(&self) -> Result<Buf, Error> {
|
|
self.remote.default_branch()
|
|
}
|
|
|
|
/// access remote bound to this connection
|
|
pub fn remote(&mut self) -> &mut Remote<'repo> {
|
|
self.remote
|
|
}
|
|
}
|
|
|
|
impl<'repo, 'connection, 'cb> Drop for RemoteConnection<'repo, 'connection, 'cb> {
|
|
fn drop(&mut self) {
|
|
drop(self.remote.disconnect());
|
|
}
|
|
}
|
|
|
|
impl Default for RemoteRedirect {
|
|
fn default() -> Self {
|
|
RemoteRedirect::Initial
|
|
}
|
|
}
|
|
|
|
impl RemoteRedirect {
|
|
fn raw(&self) -> raw::git_remote_redirect_t {
|
|
match self {
|
|
RemoteRedirect::None => raw::GIT_REMOTE_REDIRECT_NONE,
|
|
RemoteRedirect::Initial => raw::GIT_REMOTE_REDIRECT_INITIAL,
|
|
RemoteRedirect::All => raw::GIT_REMOTE_REDIRECT_ALL,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::{AutotagOption, PushOptions};
|
|
use crate::{Direction, FetchOptions, Remote, RemoteCallbacks, Repository};
|
|
use std::cell::Cell;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn smoke() {
|
|
let (td, repo) = crate::test::repo_init();
|
|
t!(repo.remote("origin", "/path/to/nowhere"));
|
|
drop(repo);
|
|
|
|
let repo = t!(Repository::init(td.path()));
|
|
let mut origin = t!(repo.find_remote("origin"));
|
|
assert_eq!(origin.name(), Some("origin"));
|
|
assert_eq!(origin.url(), Some("/path/to/nowhere"));
|
|
assert_eq!(origin.pushurl(), None);
|
|
|
|
t!(repo.remote_set_url("origin", "/path/to/elsewhere"));
|
|
t!(repo.remote_set_pushurl("origin", Some("/path/to/elsewhere")));
|
|
|
|
let stats = origin.stats();
|
|
assert_eq!(stats.total_objects(), 0);
|
|
|
|
t!(origin.stop());
|
|
}
|
|
|
|
#[test]
|
|
fn create_remote() {
|
|
let td = TempDir::new().unwrap();
|
|
let remote = td.path().join("remote");
|
|
Repository::init_bare(&remote).unwrap();
|
|
|
|
let (_td, repo) = crate::test::repo_init();
|
|
let url = if cfg!(unix) {
|
|
format!("file://{}", remote.display())
|
|
} else {
|
|
format!(
|
|
"file:///{}",
|
|
remote.display().to_string().replace("\\", "/")
|
|
)
|
|
};
|
|
|
|
let mut origin = repo.remote("origin", &url).unwrap();
|
|
assert_eq!(origin.name(), Some("origin"));
|
|
assert_eq!(origin.url(), Some(&url[..]));
|
|
assert_eq!(origin.pushurl(), None);
|
|
|
|
{
|
|
let mut specs = origin.refspecs();
|
|
let spec = specs.next().unwrap();
|
|
assert!(specs.next().is_none());
|
|
assert_eq!(spec.str(), Some("+refs/heads/*:refs/remotes/origin/*"));
|
|
assert_eq!(spec.dst(), Some("refs/remotes/origin/*"));
|
|
assert_eq!(spec.src(), Some("refs/heads/*"));
|
|
assert!(spec.is_force());
|
|
}
|
|
assert!(origin.refspecs().next_back().is_some());
|
|
{
|
|
let remotes = repo.remotes().unwrap();
|
|
assert_eq!(remotes.len(), 1);
|
|
assert_eq!(remotes.get(0), Some("origin"));
|
|
assert_eq!(remotes.iter().count(), 1);
|
|
assert_eq!(remotes.iter().next().unwrap(), Some("origin"));
|
|
}
|
|
|
|
origin.connect(Direction::Push).unwrap();
|
|
assert!(origin.connected());
|
|
origin.disconnect().unwrap();
|
|
|
|
origin.connect(Direction::Fetch).unwrap();
|
|
assert!(origin.connected());
|
|
origin.download(&[] as &[&str], None).unwrap();
|
|
origin.disconnect().unwrap();
|
|
|
|
{
|
|
let mut connection = origin.connect_auth(Direction::Push, None, None).unwrap();
|
|
assert!(connection.connected());
|
|
}
|
|
assert!(!origin.connected());
|
|
|
|
{
|
|
let mut connection = origin.connect_auth(Direction::Fetch, None, None).unwrap();
|
|
assert!(connection.connected());
|
|
}
|
|
assert!(!origin.connected());
|
|
|
|
origin.fetch(&[] as &[&str], None, None).unwrap();
|
|
origin.fetch(&[] as &[&str], None, Some("foo")).unwrap();
|
|
origin
|
|
.update_tips(None, true, AutotagOption::Unspecified, None)
|
|
.unwrap();
|
|
origin
|
|
.update_tips(None, true, AutotagOption::All, Some("foo"))
|
|
.unwrap();
|
|
|
|
t!(repo.remote_add_fetch("origin", "foo"));
|
|
t!(repo.remote_add_fetch("origin", "bar"));
|
|
}
|
|
|
|
#[test]
|
|
fn rename_remote() {
|
|
let (_td, repo) = crate::test::repo_init();
|
|
repo.remote("origin", "foo").unwrap();
|
|
drop(repo.remote_rename("origin", "foo"));
|
|
drop(repo.remote_delete("foo"));
|
|
}
|
|
|
|
#[test]
|
|
fn create_remote_anonymous() {
|
|
let td = TempDir::new().unwrap();
|
|
let repo = Repository::init(td.path()).unwrap();
|
|
|
|
let origin = repo.remote_anonymous("/path/to/nowhere").unwrap();
|
|
assert_eq!(origin.name(), None);
|
|
drop(origin.clone());
|
|
}
|
|
|
|
#[test]
|
|
fn is_valid_name() {
|
|
assert!(Remote::is_valid_name("foobar"));
|
|
assert!(!Remote::is_valid_name("\x01"));
|
|
}
|
|
|
|
#[test]
|
|
#[should_panic]
|
|
fn is_valid_name_for_invalid_remote() {
|
|
Remote::is_valid_name("ab\012");
|
|
}
|
|
|
|
#[test]
|
|
fn transfer_cb() {
|
|
let (td, _repo) = crate::test::repo_init();
|
|
let td2 = TempDir::new().unwrap();
|
|
let url = crate::test::path2url(&td.path());
|
|
|
|
let repo = Repository::init(td2.path()).unwrap();
|
|
let progress_hit = Cell::new(false);
|
|
{
|
|
let mut callbacks = RemoteCallbacks::new();
|
|
let mut origin = repo.remote("origin", &url).unwrap();
|
|
|
|
callbacks.transfer_progress(|_progress| {
|
|
progress_hit.set(true);
|
|
true
|
|
});
|
|
origin
|
|
.fetch(
|
|
&[] as &[&str],
|
|
Some(FetchOptions::new().remote_callbacks(callbacks)),
|
|
None,
|
|
)
|
|
.unwrap();
|
|
|
|
let list = t!(origin.list());
|
|
assert_eq!(list.len(), 2);
|
|
assert_eq!(list[0].name(), "HEAD");
|
|
assert!(!list[0].is_local());
|
|
assert_eq!(list[1].name(), "refs/heads/main");
|
|
assert!(!list[1].is_local());
|
|
}
|
|
assert!(progress_hit.get());
|
|
}
|
|
|
|
/// This test is meant to assure that the callbacks provided to connect will not cause
|
|
/// segfaults
|
|
#[test]
|
|
fn connect_list() {
|
|
let (td, _repo) = crate::test::repo_init();
|
|
let td2 = TempDir::new().unwrap();
|
|
let url = crate::test::path2url(&td.path());
|
|
|
|
let repo = Repository::init(td2.path()).unwrap();
|
|
let mut callbacks = RemoteCallbacks::new();
|
|
callbacks.sideband_progress(|_progress| {
|
|
// no-op
|
|
true
|
|
});
|
|
|
|
let mut origin = repo.remote("origin", &url).unwrap();
|
|
|
|
{
|
|
let mut connection = origin
|
|
.connect_auth(Direction::Fetch, Some(callbacks), None)
|
|
.unwrap();
|
|
assert!(connection.connected());
|
|
|
|
let list = t!(connection.list());
|
|
assert_eq!(list.len(), 2);
|
|
assert_eq!(list[0].name(), "HEAD");
|
|
assert!(!list[0].is_local());
|
|
assert_eq!(list[1].name(), "refs/heads/main");
|
|
assert!(!list[1].is_local());
|
|
}
|
|
assert!(!origin.connected());
|
|
}
|
|
|
|
#[test]
|
|
fn push() {
|
|
let (_td, repo) = crate::test::repo_init();
|
|
let td2 = TempDir::new().unwrap();
|
|
let td3 = TempDir::new().unwrap();
|
|
let url = crate::test::path2url(&td2.path());
|
|
|
|
let mut opts = crate::RepositoryInitOptions::new();
|
|
opts.bare(true);
|
|
opts.initial_head("main");
|
|
Repository::init_opts(td2.path(), &opts).unwrap();
|
|
// git push
|
|
let mut remote = repo.remote("origin", &url).unwrap();
|
|
let mut updated = false;
|
|
{
|
|
let mut callbacks = RemoteCallbacks::new();
|
|
callbacks.push_update_reference(|refname, status| {
|
|
updated = true;
|
|
assert_eq!(refname, "refs/heads/main");
|
|
assert_eq!(status, None);
|
|
Ok(())
|
|
});
|
|
let mut options = PushOptions::new();
|
|
options.remote_callbacks(callbacks);
|
|
remote
|
|
.push(&["refs/heads/main"], Some(&mut options))
|
|
.unwrap();
|
|
}
|
|
assert!(updated);
|
|
|
|
let repo = Repository::clone(&url, td3.path()).unwrap();
|
|
let commit = repo.head().unwrap().target().unwrap();
|
|
let commit = repo.find_commit(commit).unwrap();
|
|
assert_eq!(commit.message(), Some("initial\n\nbody"));
|
|
}
|
|
|
|
#[test]
|
|
fn prune() {
|
|
let (td, remote_repo) = crate::test::repo_init();
|
|
let oid = remote_repo.head().unwrap().target().unwrap();
|
|
let commit = remote_repo.find_commit(oid).unwrap();
|
|
remote_repo.branch("stale", &commit, true).unwrap();
|
|
|
|
let td2 = TempDir::new().unwrap();
|
|
let url = crate::test::path2url(&td.path());
|
|
let repo = Repository::clone(&url, &td2).unwrap();
|
|
|
|
fn assert_branch_count(repo: &Repository, count: usize) {
|
|
assert_eq!(
|
|
repo.branches(Some(crate::BranchType::Remote))
|
|
.unwrap()
|
|
.filter(|b| b.as_ref().unwrap().0.name().unwrap() == Some("origin/stale"))
|
|
.count(),
|
|
count,
|
|
);
|
|
}
|
|
|
|
assert_branch_count(&repo, 1);
|
|
|
|
// delete `stale` branch on remote repo
|
|
let mut stale_branch = remote_repo
|
|
.find_branch("stale", crate::BranchType::Local)
|
|
.unwrap();
|
|
stale_branch.delete().unwrap();
|
|
|
|
// prune
|
|
let mut remote = repo.find_remote("origin").unwrap();
|
|
remote.connect(Direction::Push).unwrap();
|
|
let mut callbacks = RemoteCallbacks::new();
|
|
callbacks.update_tips(|refname, _a, b| {
|
|
assert_eq!(refname, "refs/remotes/origin/stale");
|
|
assert!(b.is_zero());
|
|
true
|
|
});
|
|
remote.prune(Some(callbacks)).unwrap();
|
|
assert_branch_count(&repo, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn push_negotiation() {
|
|
let (_td, repo) = crate::test::repo_init();
|
|
let oid = repo.head().unwrap().target().unwrap();
|
|
|
|
let td2 = TempDir::new().unwrap();
|
|
let url = crate::test::path2url(td2.path());
|
|
let mut opts = crate::RepositoryInitOptions::new();
|
|
opts.bare(true);
|
|
opts.initial_head("main");
|
|
let remote_repo = Repository::init_opts(td2.path(), &opts).unwrap();
|
|
|
|
// reject pushing a branch
|
|
let mut remote = repo.remote("origin", &url).unwrap();
|
|
let mut updated = false;
|
|
{
|
|
let mut callbacks = RemoteCallbacks::new();
|
|
callbacks.push_negotiation(|updates| {
|
|
assert!(!updated);
|
|
updated = true;
|
|
assert_eq!(updates.len(), 1);
|
|
let u = &updates[0];
|
|
assert_eq!(u.src_refname().unwrap(), "refs/heads/main");
|
|
assert!(u.src().is_zero());
|
|
assert_eq!(u.dst_refname().unwrap(), "refs/heads/main");
|
|
assert_eq!(u.dst(), oid);
|
|
Err(crate::Error::from_str("rejected"))
|
|
});
|
|
let mut options = PushOptions::new();
|
|
options.remote_callbacks(callbacks);
|
|
assert!(remote
|
|
.push(&["refs/heads/main"], Some(&mut options))
|
|
.is_err());
|
|
}
|
|
assert!(updated);
|
|
assert_eq!(remote_repo.branches(None).unwrap().count(), 0);
|
|
|
|
// push 3 branches
|
|
let commit = repo.find_commit(oid).unwrap();
|
|
repo.branch("new1", &commit, true).unwrap();
|
|
repo.branch("new2", &commit, true).unwrap();
|
|
let mut flag = 0;
|
|
updated = false;
|
|
{
|
|
let mut callbacks = RemoteCallbacks::new();
|
|
callbacks.push_negotiation(|updates| {
|
|
assert!(!updated);
|
|
updated = true;
|
|
assert_eq!(updates.len(), 3);
|
|
for u in updates {
|
|
assert!(u.src().is_zero());
|
|
assert_eq!(u.dst(), oid);
|
|
let src_name = u.src_refname().unwrap();
|
|
let dst_name = u.dst_refname().unwrap();
|
|
match src_name {
|
|
"refs/heads/main" => {
|
|
assert_eq!(dst_name, src_name);
|
|
flag |= 1;
|
|
}
|
|
"refs/heads/new1" => {
|
|
assert_eq!(dst_name, "refs/heads/dev1");
|
|
flag |= 2;
|
|
}
|
|
"refs/heads/new2" => {
|
|
assert_eq!(dst_name, "refs/heads/dev2");
|
|
flag |= 4;
|
|
}
|
|
_ => panic!("unexpected refname: {}", src_name),
|
|
}
|
|
}
|
|
Ok(())
|
|
});
|
|
let mut options = PushOptions::new();
|
|
options.remote_callbacks(callbacks);
|
|
remote
|
|
.push(
|
|
&[
|
|
"refs/heads/main",
|
|
"refs/heads/new1:refs/heads/dev1",
|
|
"refs/heads/new2:refs/heads/dev2",
|
|
],
|
|
Some(&mut options),
|
|
)
|
|
.unwrap();
|
|
}
|
|
assert!(updated);
|
|
assert_eq!(flag, 7);
|
|
assert_eq!(remote_repo.branches(None).unwrap().count(), 3);
|
|
}
|
|
}
|