Skip to content

Add bindings for push_negotiation callback #926

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ pub use crate::patch::Patch;
pub use crate::pathspec::{Pathspec, PathspecFailedEntries, PathspecMatchList};
pub use crate::pathspec::{PathspecDiffEntries, PathspecEntries};
pub use crate::proxy_options::ProxyOptions;
pub use crate::push_update::PushUpdate;
pub use crate::rebase::{Rebase, RebaseOperation, RebaseOperationType, RebaseOptions};
pub use crate::reference::{Reference, ReferenceNames, References};
pub use crate::reflog::{Reflog, ReflogEntry, ReflogIter};
Expand Down Expand Up @@ -694,6 +695,7 @@ mod packbuilder;
mod patch;
mod pathspec;
mod proxy_options;
mod push_update;
mod rebase;
mod reference;
mod reflog;
Expand Down
55 changes: 55 additions & 0 deletions src/push_update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::util::Binding;
use crate::{raw, Oid};
use std::marker;
use std::str;

/// Represents an update which will be performed on the remote during push.
pub struct PushUpdate<'a> {
raw: *const raw::git_push_update,
_marker: marker::PhantomData<&'a raw::git_push_update>,
}

impl<'a> Binding for PushUpdate<'a> {
type Raw = *const raw::git_push_update;
unsafe fn from_raw(raw: *const raw::git_push_update) -> PushUpdate<'a> {
PushUpdate {
raw,
_marker: marker::PhantomData,
}
}
fn raw(&self) -> Self::Raw {
self.raw
}
}

impl PushUpdate<'_> {
/// Returns the source name of the reference as a byte slice.
pub fn src_refname_bytes(&self) -> &[u8] {
unsafe { crate::opt_bytes(self, (*self.raw).src_refname).unwrap() }
}

/// Returns the source name of the reference.
pub fn src_refname(&self) -> Option<&str> {
str::from_utf8(self.src_refname_bytes()).ok()
}

/// Returns the destination name of the reference as a byte slice.
pub fn dst_refname_bytes(&self) -> &[u8] {
unsafe { crate::opt_bytes(self, (*self.raw).dst_refname).unwrap() }
}

/// Returns the destination name of the reference.
pub fn dst_refname(&self) -> Option<&str> {
str::from_utf8(self.dst_refname_bytes()).ok()
}

/// Returns the current target of the reference.
pub fn src(&self) -> Oid {
unsafe { Binding::from_raw(&(*self.raw).src as *const _) }
}

/// Returns the new target for the reference.
pub fn dst(&self) -> Oid {
unsafe { Binding::from_raw(&(*self.raw).dst as *const _) }
}
}
90 changes: 90 additions & 0 deletions src/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,4 +1014,94 @@ mod tests {
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);
}
}
45 changes: 45 additions & 0 deletions src/remote_callbacks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::cert::Cert;
use crate::util::Binding;
use crate::{
panic, raw, Cred, CredentialType, Error, IndexerProgress, Oid, PackBuilderStage, Progress,
PushUpdate,
};

/// A structure to contain the callbacks which are invoked when a repository is
Expand All @@ -25,6 +26,7 @@ pub struct RemoteCallbacks<'a> {
update_tips: Option<Box<UpdateTips<'a>>>,
certificate_check: Option<Box<CertificateCheck<'a>>>,
push_update_reference: Option<Box<PushUpdateReference<'a>>>,
push_negotiation: Option<Box<PushNegotiation<'a>>>,
}

/// Callback used to acquire credentials for when a remote is fetched.
Expand Down Expand Up @@ -87,6 +89,14 @@ pub type PushTransferProgress<'a> = dyn FnMut(usize, usize, usize) + 'a;
/// * total
pub type PackProgress<'a> = dyn FnMut(PackBuilderStage, usize, usize) + 'a;

/// Callback used to inform of upcoming updates.
///
/// The argument is a slice containing the updates which will be sent as
/// commands to the destination.
///
/// The push is cancelled if an error is returned.
pub type PushNegotiation<'a> = dyn FnMut(&[PushUpdate<'_>]) -> Result<(), Error> + 'a;

impl<'a> Default for RemoteCallbacks<'a> {
fn default() -> Self {
Self::new()
Expand All @@ -105,6 +115,7 @@ impl<'a> RemoteCallbacks<'a> {
certificate_check: None,
push_update_reference: None,
push_progress: None,
push_negotiation: None,
}
}

Expand Down Expand Up @@ -211,6 +222,16 @@ impl<'a> RemoteCallbacks<'a> {
self.pack_progress = Some(Box::new(cb) as Box<PackProgress<'a>>);
self
}

/// The callback is called once between the negotiation step and the upload.
/// It provides information about what updates will be performed.
pub fn push_negotiation<F>(&mut self, cb: F) -> &mut RemoteCallbacks<'a>
where
F: FnMut(&[PushUpdate<'_>]) -> Result<(), Error> + 'a,
{
self.push_negotiation = Some(Box::new(cb) as Box<PushNegotiation<'a>>);
self
}
}

impl<'a> Binding for RemoteCallbacks<'a> {
Expand Down Expand Up @@ -256,6 +277,9 @@ impl<'a> Binding for RemoteCallbacks<'a> {
) -> c_int = update_tips_cb;
callbacks.update_tips = Some(f);
}
if self.push_negotiation.is_some() {
callbacks.push_negotiation = Some(push_negotiation_cb);
}
callbacks.payload = self as *const _ as *mut _;
callbacks
}
Expand Down Expand Up @@ -471,3 +495,24 @@ extern "C" fn pack_progress_cb(
})
.unwrap_or(-1)
}

extern "C" fn push_negotiation_cb(
updates: *mut *const raw::git_push_update,
len: size_t,
payload: *mut c_void,
) -> c_int {
panic::wrap(|| unsafe {
let payload = &mut *(payload as *mut RemoteCallbacks<'_>);
let callback = match payload.push_negotiation {
Some(ref mut c) => c,
None => return 0,
};

let updates = slice::from_raw_parts(updates as *mut PushUpdate<'_>, len);
match callback(updates) {
Ok(()) => 0,
Err(e) => e.raw_code(),
}
})
.unwrap_or(-1)
}