//! git_apply support //! see original: use crate::{panic, raw, util::Binding, DiffDelta, DiffHunk}; use libc::c_int; use std::{ffi::c_void, mem}; /// Possible application locations for git_apply /// see #[derive(Copy, Clone, Debug)] pub enum ApplyLocation { /// Apply the patch to the workdir WorkDir, /// Apply the patch to the index Index, /// Apply the patch to both the working directory and the index Both, } impl Binding for ApplyLocation { type Raw = raw::git_apply_location_t; unsafe fn from_raw(raw: raw::git_apply_location_t) -> Self { match raw { raw::GIT_APPLY_LOCATION_WORKDIR => Self::WorkDir, raw::GIT_APPLY_LOCATION_INDEX => Self::Index, raw::GIT_APPLY_LOCATION_BOTH => Self::Both, _ => panic!("Unknown git diff binary kind"), } } fn raw(&self) -> raw::git_apply_location_t { match *self { Self::WorkDir => raw::GIT_APPLY_LOCATION_WORKDIR, Self::Index => raw::GIT_APPLY_LOCATION_INDEX, Self::Both => raw::GIT_APPLY_LOCATION_BOTH, } } } /// Options to specify when applying a diff pub struct ApplyOptions<'cb> { raw: raw::git_apply_options, hunk_cb: Option>>, delta_cb: Option>>, } type HunkCB<'a> = dyn FnMut(Option>) -> bool + 'a; type DeltaCB<'a> = dyn FnMut(Option>) -> bool + 'a; extern "C" fn delta_cb_c(delta: *const raw::git_diff_delta, data: *mut c_void) -> c_int { panic::wrap(|| unsafe { let delta = Binding::from_raw_opt(delta as *mut _); let payload = &mut *(data as *mut ApplyOptions<'_>); let callback = match payload.delta_cb { Some(ref mut c) => c, None => return -1, }; let apply = callback(delta); if apply { 0 } else { 1 } }) .unwrap_or(-1) } extern "C" fn hunk_cb_c(hunk: *const raw::git_diff_hunk, data: *mut c_void) -> c_int { panic::wrap(|| unsafe { let hunk = Binding::from_raw_opt(hunk); let payload = &mut *(data as *mut ApplyOptions<'_>); let callback = match payload.hunk_cb { Some(ref mut c) => c, None => return -1, }; let apply = callback(hunk); if apply { 0 } else { 1 } }) .unwrap_or(-1) } impl<'cb> ApplyOptions<'cb> { /// Creates a new set of empty options (zeroed). pub fn new() -> Self { let mut opts = Self { raw: unsafe { mem::zeroed() }, hunk_cb: None, delta_cb: None, }; assert_eq!( unsafe { raw::git_apply_options_init(&mut opts.raw, raw::GIT_APPLY_OPTIONS_VERSION) }, 0 ); opts } fn flag(&mut self, opt: raw::git_apply_flags_t, val: bool) -> &mut Self { let opt = opt as u32; if val { self.raw.flags |= opt; } else { self.raw.flags &= !opt; } self } /// Don't actually make changes, just test that the patch applies. pub fn check(&mut self, check: bool) -> &mut Self { self.flag(raw::GIT_APPLY_CHECK, check) } /// When applying a patch, callback that will be made per hunk. pub fn hunk_callback(&mut self, cb: F) -> &mut Self where F: FnMut(Option>) -> bool + 'cb, { self.hunk_cb = Some(Box::new(cb) as Box>); self.raw.hunk_cb = Some(hunk_cb_c); self.raw.payload = self as *mut _ as *mut _; self } /// When applying a patch, callback that will be made per delta (file). pub fn delta_callback(&mut self, cb: F) -> &mut Self where F: FnMut(Option>) -> bool + 'cb, { self.delta_cb = Some(Box::new(cb) as Box>); self.raw.delta_cb = Some(delta_cb_c); self.raw.payload = self as *mut _ as *mut _; self } /// Pointer to a raw git_stash_apply_options pub unsafe fn raw(&mut self) -> *const raw::git_apply_options { &self.raw as *const _ } } #[cfg(test)] mod tests { use super::*; use std::{fs::File, io::Write, path::Path}; #[test] fn smoke_test() { let (_td, repo) = crate::test::repo_init(); let diff = t!(repo.diff_tree_to_workdir(None, None)); let mut count_hunks = 0; let mut count_delta = 0; { let mut opts = ApplyOptions::new(); opts.hunk_callback(|_hunk| { count_hunks += 1; true }); opts.delta_callback(|_delta| { count_delta += 1; true }); t!(repo.apply(&diff, ApplyLocation::Both, Some(&mut opts))); } assert_eq!(count_hunks, 0); assert_eq!(count_delta, 0); } #[test] fn apply_hunks_and_delta() { let file_path = Path::new("foo.txt"); let (td, repo) = crate::test::repo_init(); // create new file t!(t!(File::create(&td.path().join(file_path))).write_all(b"bar")); // stage the new file t!(t!(repo.index()).add_path(file_path)); // now change workdir version t!(t!(File::create(&td.path().join(file_path))).write_all(b"foo\nbar")); let diff = t!(repo.diff_index_to_workdir(None, None)); assert_eq!(diff.deltas().len(), 1); let mut count_hunks = 0; let mut count_delta = 0; { let mut opts = ApplyOptions::new(); opts.hunk_callback(|_hunk| { count_hunks += 1; true }); opts.delta_callback(|_delta| { count_delta += 1; true }); t!(repo.apply(&diff, ApplyLocation::Index, Some(&mut opts))); } assert_eq!(count_delta, 1); assert_eq!(count_hunks, 1); } }