Skip to content

Commit

Permalink
Merge pull request #926 from karin0/push_negotiation
Browse files Browse the repository at this point in the history
Add bindings for push_negotiation callback
  • Loading branch information
ehuss authored Apr 2, 2023
2 parents 76897b8 + a638e23 commit 88a9817
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 0 deletions.
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)
}

0 comments on commit 88a9817

Please sign in to comment.