1 //! git_apply support
2 //! see original: <https://github.com/libgit2/libgit2/blob/master/include/git2/apply.h>
3 
4 use crate::{panic, raw, util::Binding, DiffDelta, DiffHunk};
5 use libc::c_int;
6 use std::{ffi::c_void, mem};
7 
8 /// Possible application locations for git_apply
9 /// see <https://libgit2.org/libgit2/#HEAD/type/git_apply_options>
10 #[derive(Copy, Clone, Debug)]
11 pub enum ApplyLocation {
12     /// Apply the patch to the workdir
13     WorkDir,
14     /// Apply the patch to the index
15     Index,
16     /// Apply the patch to both the working directory and the index
17     Both,
18 }
19 
20 impl Binding for ApplyLocation {
21     type Raw = raw::git_apply_location_t;
from_raw(raw: raw::git_apply_location_t) -> Self22     unsafe fn from_raw(raw: raw::git_apply_location_t) -> Self {
23         match raw {
24             raw::GIT_APPLY_LOCATION_WORKDIR => Self::WorkDir,
25             raw::GIT_APPLY_LOCATION_INDEX => Self::Index,
26             raw::GIT_APPLY_LOCATION_BOTH => Self::Both,
27             _ => panic!("Unknown git diff binary kind"),
28         }
29     }
raw(&self) -> raw::git_apply_location_t30     fn raw(&self) -> raw::git_apply_location_t {
31         match *self {
32             Self::WorkDir => raw::GIT_APPLY_LOCATION_WORKDIR,
33             Self::Index => raw::GIT_APPLY_LOCATION_INDEX,
34             Self::Both => raw::GIT_APPLY_LOCATION_BOTH,
35         }
36     }
37 }
38 
39 /// Options to specify when applying a diff
40 pub struct ApplyOptions<'cb> {
41     raw: raw::git_apply_options,
42     hunk_cb: Option<Box<HunkCB<'cb>>>,
43     delta_cb: Option<Box<DeltaCB<'cb>>>,
44 }
45 
46 type HunkCB<'a> = dyn FnMut(Option<DiffHunk<'_>>) -> bool + 'a;
47 type DeltaCB<'a> = dyn FnMut(Option<DiffDelta<'_>>) -> bool + 'a;
48 
delta_cb_c(delta: *const raw::git_diff_delta, data: *mut c_void) -> c_int49 extern "C" fn delta_cb_c(delta: *const raw::git_diff_delta, data: *mut c_void) -> c_int {
50     panic::wrap(|| unsafe {
51         let delta = Binding::from_raw_opt(delta as *mut _);
52 
53         let payload = &mut *(data as *mut ApplyOptions<'_>);
54         let callback = match payload.delta_cb {
55             Some(ref mut c) => c,
56             None => return -1,
57         };
58 
59         let apply = callback(delta);
60         if apply {
61             0
62         } else {
63             1
64         }
65     })
66     .unwrap_or(-1)
67 }
68 
hunk_cb_c(hunk: *const raw::git_diff_hunk, data: *mut c_void) -> c_int69 extern "C" fn hunk_cb_c(hunk: *const raw::git_diff_hunk, data: *mut c_void) -> c_int {
70     panic::wrap(|| unsafe {
71         let hunk = Binding::from_raw_opt(hunk);
72 
73         let payload = &mut *(data as *mut ApplyOptions<'_>);
74         let callback = match payload.hunk_cb {
75             Some(ref mut c) => c,
76             None => return -1,
77         };
78 
79         let apply = callback(hunk);
80         if apply {
81             0
82         } else {
83             1
84         }
85     })
86     .unwrap_or(-1)
87 }
88 
89 impl<'cb> ApplyOptions<'cb> {
90     /// Creates a new set of empty options (zeroed).
new() -> Self91     pub fn new() -> Self {
92         let mut opts = Self {
93             raw: unsafe { mem::zeroed() },
94             hunk_cb: None,
95             delta_cb: None,
96         };
97         assert_eq!(
98             unsafe { raw::git_apply_options_init(&mut opts.raw, raw::GIT_APPLY_OPTIONS_VERSION) },
99             0
100         );
101         opts
102     }
103 
flag(&mut self, opt: raw::git_apply_flags_t, val: bool) -> &mut Self104     fn flag(&mut self, opt: raw::git_apply_flags_t, val: bool) -> &mut Self {
105         let opt = opt as u32;
106         if val {
107             self.raw.flags |= opt;
108         } else {
109             self.raw.flags &= !opt;
110         }
111         self
112     }
113 
114     /// Don't actually make changes, just test that the patch applies.
check(&mut self, check: bool) -> &mut Self115     pub fn check(&mut self, check: bool) -> &mut Self {
116         self.flag(raw::GIT_APPLY_CHECK, check)
117     }
118 
119     /// When applying a patch, callback that will be made per hunk.
hunk_callback<F>(&mut self, cb: F) -> &mut Self where F: FnMut(Option<DiffHunk<'_>>) -> bool + 'cb,120     pub fn hunk_callback<F>(&mut self, cb: F) -> &mut Self
121     where
122         F: FnMut(Option<DiffHunk<'_>>) -> bool + 'cb,
123     {
124         self.hunk_cb = Some(Box::new(cb) as Box<HunkCB<'cb>>);
125 
126         self.raw.hunk_cb = Some(hunk_cb_c);
127         self.raw.payload = self as *mut _ as *mut _;
128 
129         self
130     }
131 
132     /// When applying a patch, callback that will be made per delta (file).
delta_callback<F>(&mut self, cb: F) -> &mut Self where F: FnMut(Option<DiffDelta<'_>>) -> bool + 'cb,133     pub fn delta_callback<F>(&mut self, cb: F) -> &mut Self
134     where
135         F: FnMut(Option<DiffDelta<'_>>) -> bool + 'cb,
136     {
137         self.delta_cb = Some(Box::new(cb) as Box<DeltaCB<'cb>>);
138 
139         self.raw.delta_cb = Some(delta_cb_c);
140         self.raw.payload = self as *mut _ as *mut _;
141 
142         self
143     }
144 
145     /// Pointer to a raw git_stash_apply_options
raw(&mut self) -> *const raw::git_apply_options146     pub unsafe fn raw(&mut self) -> *const raw::git_apply_options {
147         &self.raw as *const _
148     }
149 }
150 
151 #[cfg(test)]
152 mod tests {
153     use super::*;
154     use std::{fs::File, io::Write, path::Path};
155 
156     #[test]
smoke_test()157     fn smoke_test() {
158         let (_td, repo) = crate::test::repo_init();
159         let diff = t!(repo.diff_tree_to_workdir(None, None));
160         let mut count_hunks = 0;
161         let mut count_delta = 0;
162         {
163             let mut opts = ApplyOptions::new();
164             opts.hunk_callback(|_hunk| {
165                 count_hunks += 1;
166                 true
167             });
168             opts.delta_callback(|_delta| {
169                 count_delta += 1;
170                 true
171             });
172             t!(repo.apply(&diff, ApplyLocation::Both, Some(&mut opts)));
173         }
174         assert_eq!(count_hunks, 0);
175         assert_eq!(count_delta, 0);
176     }
177 
178     #[test]
apply_hunks_and_delta()179     fn apply_hunks_and_delta() {
180         let file_path = Path::new("foo.txt");
181         let (td, repo) = crate::test::repo_init();
182         // create new file
183         t!(t!(File::create(&td.path().join(file_path))).write_all(b"bar"));
184         // stage the new file
185         t!(t!(repo.index()).add_path(file_path));
186         // now change workdir version
187         t!(t!(File::create(&td.path().join(file_path))).write_all(b"foo\nbar"));
188 
189         let diff = t!(repo.diff_index_to_workdir(None, None));
190         assert_eq!(diff.deltas().len(), 1);
191         let mut count_hunks = 0;
192         let mut count_delta = 0;
193         {
194             let mut opts = ApplyOptions::new();
195             opts.hunk_callback(|_hunk| {
196                 count_hunks += 1;
197                 true
198             });
199             opts.delta_callback(|_delta| {
200                 count_delta += 1;
201                 true
202             });
203             t!(repo.apply(&diff, ApplyLocation::Index, Some(&mut opts)));
204         }
205         assert_eq!(count_delta, 1);
206         assert_eq!(count_hunks, 1);
207     }
208 }
209